Files
Terminal.Gui/Tests/TerminalGuiFluentTesting/GuiTestContext.cs
Copilot 2be8397203 Remove legacy drivers, simplify architecture, and reorganize codebase structure (#4270)
* Initial plan

* Remove legacy drivers and reorganize v2 architecture

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Extract Windows key helper utilities and fix build

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fix all test references to legacy drivers

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Update documentation to reflect new driver architecture

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Remove V2.cd diagram file

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fix test failures: support legacy drivers and update exception handling

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* updated driver names

* Move V2 tests from ConsoleDrivers/V2 to proper locations

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Rename ApplicationV2 to ModernApplicationImpl to remove v2 terminology

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Remove V2 terminology from test drivers and FakeDriver classes

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Merge ModernApplicationImpl into ApplicationImpl and move to App folder

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Create modern FakeDriver with component factory architecture in Terminal.Gui project

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Refactor: Move non-platform-dependent code from /Drivers to /App

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Code cleanup and org

* Unit test reorg

* Refactor MainLoop architecture: rename classes and enhance documentation for clarity

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add comprehensive FakeDriver tests (WIP - some tests need fixes)

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fixed FakeDriver build failures

* Fix all FakeDriver test failures - Application.Top creation and clipboard behaviors

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fixed FakeDriver build failures2

* Remove hanging legacy FakeDriver tests that use Console.MockKeyPresses

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fixed some tests

* Fixed more tests

* Fixed more tests

* Fix bad copilot (#4277)

* Update Terminal.Gui/Drivers/FakeDriver/FakeConsoleOutput.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor Application Init and Update Tests

Refactored `Application.Init` to improve initialization logic:
- Added fallback to `ForceDriver` when `driverName` is null.
- Changed repeated `Init` calls to throw `InvalidOperationException`.
- Updated `_driverName` assignment logic for robustness.

Enhanced `IConsoleDriver` with detailed remarks on implementations.

Revised test cases to align with updated `Application.Init` behavior:
- Replaced `FakeDriver` with `null` and `driverName: "fake"`.
- Skipped or commented out tests incompatible with new logic.
- Improved formatting and removed redundant setup code.

Improved code style and consistency across the codebase:
- Standardized parameter formatting and spacing.
- Removed outdated comments and unused code.

General cleanup to enhance readability and maintainability.

* Warp fix copilot (#4278)

* More fixes (#4279)

* Fixes/works around test failures and temporarily disable failing test

Updated `FakeDriver` to set `RunningUnitTests` to `true` and initialize dimensions using `FakeConsole`. Modified `TestRespondersDisposedAttribute` to set `ConsoleDriver.RunningUnitTests` in the `Before` method, ensuring proper behavior during unit tests.

Temporarily disabled the `Button_CanFocus_False_Raises_Accepted_Correctly` test in `ViewCommandTests` by adding a `Skip` parameter to the `[Fact]` attribute, referencing issue #4270.

* Allow all tests to run despite failures in UnitTests

Modified the `dotnet test` command in the `Run UnitTestsParallelizable` step to set `xunit.stopOnFail` to `false`. This ensures that the test runner does not stop execution on the first failure, allowing all tests to execute regardless of individual test outcomes.

* Refactor ApplicationScreenTests for cleaner setup/teardown

Refactored `ClearContents_Called_When_Top_Frame_Changes` test:
- Added `[AutoInitShutdown]` attribute for automatic lifecycle management.
- Replaced manual `Application.Init` and `Application.Top` setup with `Application.Begin` and `RunState`.
- Simplified event handling by defining `ClearedContents` handler inline.
- Removed explicit cleanup logic, relying on `Application.End` for teardown.

Updated `using` directives to include `UnitTests` namespace.

* Attempt to fix intermittent local test failures.

Update ApplicationImpl initialization parameter

Changed the second parameter of the `impl.Init` method in the
`FakeApplicationFactory` class from `"dotnet"` to `"fake"`.

* Code cleanup to cause Action to re-run.

* Stop tests on first failure in UnitTestsParallelizable

Updated the `dotnet test` command in `unit-tests.yml` to set the `xunit.stopOnFail` parameter to `true`. This change ensures that test execution halts immediately upon encountering a failure, allowing quicker identification and resolution of issues. Note that this may prevent the full test suite from running in the event of a failure.

* Allow all tests to run despite failures in CI

Updated `unit-tests.yml` to set `xunit.stopOnFail` to `false`
in both `Run UnitTests` and `Run UnitTestsParallelizable`
steps. This ensures that the test runner does not stop
execution on the first test failure, allowing all tests
to complete even if some fail.

* Enhance RuneExtensions docs and update user dictionary

Updated the `<remarks>` section in `RuneExtensions.GetColumns` to include details about the `wcwidth` implementation and improved readability with `<para>` tags. Added `wcwidth` to the user dictionary in `Terminal.sln.DotSettings` to avoid spelling errors.

* Improve XML doc formatting in RuneExtensions.cs

Updated the remarks section of the `GetColumns` method in the
`RuneExtensions` class to enhance readability by reformatting
and properly indenting `<para>` tags. The content remains
unchanged, describing the method's implementation via `wcwidth`
and its role as a Terminal.Gui extension for `System.Text.Rune`.

* Refactor drivers and improve clipboard handling

Replaced legacy drivers (`CursesDriver`, `NetDriver`) with
`UnixDriver` and `DotNetDriver` across the codebase, including
comments, method names, and test cases. Updated documentation
and remarks to reflect the new driver names and platforms.

Revamped clipboard handling with new platform-specific
implementations: `UnixClipboard` for Unix, `MacOSXClipboard`
for macOS, and `WSLClipboard` for Linux under WSL. Removed
the old `CursesClipboard` and consolidated clipboard logic.

Updated test cases to align with the new drivers and clipboard
implementations. Improved naming consistency and cleaned up
redundant code. Updated the README and documentation to
reflect these changes.

* Remove `PlatformColor` from `Attribute` struct

This commit removes the `PlatformColor` property from the `Attribute` struct, simplifying the codebase by eliminating platform-specific color handling. The following changes were made:

- Removed `PlatformColor` from the `Attribute` struct, including its initialization, usage, and related comments.
- Updated constructors to no longer initialize or use `PlatformColor`.
- Modified `Equals` and `GetHashCode` methods to exclude `PlatformColor`.
- Updated `UnixComponentFactory` documentation to remove references to "v2unix."
- Renamed `v2TestDriver` to `testDriver` in the `With` class for clarity.
- Removed `PlatformColor` references in `DriverAssert` and related error messages.
- Deleted test cases in `AttributeTests` that relied on `PlatformColor`.
- Cleaned up comments and TODOs related to `PlatformColor` and `UnixDriver`.

These changes reflect a shift away from platform-dependent color management, improving code clarity and reducing complexity.

Remove `PlatformColor` and simplify `Attribute` logic

The `PlatformColor` property has been removed from the `Attribute` struct, along with its associated logic, simplifying the codebase and eliminating platform-specific dependencies. Constructors, equality checks, and hash code generation in `Attribute` have been updated accordingly.

The `CurrentAttribute` property in `ConsoleDriver` and `OutputBuffer` has been simplified, removing dependencies on `Application.Driver`. The `MakeColor` method logic has been removed or simplified in related classes.

Tests in `AttributeTests` have been refactored to reflect these changes, focusing on `Foreground`, `Background`, and `Style`. Unix-specific logic tied to `PlatformColor` has been eliminated.

Additional updates include renaming parameters in the `With` class for clarity, simplifying `DriverAssert` output, and performing minor code cleanups to improve readability and maintainability.

* Refactor Terminal.Gui driver architecture for v2

Updated documentation to reflect the new modular driver architecture in Terminal.Gui v2.

- Revised `namespace-drivers.md` to include new components (`IConsoleInput`, `IConsoleOutput`, `IInputProcessor`, `IOutputBuffer`, `IWindowSizeMonitor`) and terminal size monitoring.
- Replaced "Key Components" with "Architecture Overview" and added details on the **Component Factory** pattern.
- Documented the four driver implementations (`DotNetDriver`, `WindowsDriver`, `UnixDriver`, `FakeDriver`) and their platform-specific optimizations.
- Added a "Threading Model" section to explain the multi-threaded design for responsive input handling.
- Updated examples to demonstrate driver capabilities and explicit driver selection.

In `drivers.md`:
- Expanded the "Overview" to emphasize the modular, component-based architecture.
- Reorganized "Drivers" into "Available Drivers" and added details on `FakeDriver` for unit testing.
- Added sections on "Initialization Flow," "Shutdown Flow," and platform-specific driver details.
- Provided examples for accessing driver components and creating custom drivers.

In `index.md`:
- Updated "Cross Platform" feature to reflect new driver names and clarified compatibility with SSH and monochrome terminals.

* Moved files around

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tig <585482+tig@users.noreply.github.com>
Co-authored-by: Tig <tig@users.noreply.github.com>
Co-authored-by: Thomas Nind <31306100+tznind@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-15 13:24:47 -06:00

983 lines
34 KiB
C#

using System.Diagnostics;
using System.Drawing;
using System.Text;
using Microsoft.Extensions.Logging;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
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
{
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;
private readonly object _logsLock = new ();
private readonly StringBuilder _logsSb;
private readonly TestDriver _driver;
private bool _finished;
private readonly FakeSizeMonitor _fakeSizeMonitor;
internal GuiTestContext (Func<Toplevel> topLevelBuilder, int width, int height, TestDriver driver, TextWriter? logWriter = null)
{
// Remove frame limit
Application.MaximumIterationsPerSecond = ushort.MaxValue;
IApplication origApp = ApplicationImpl.Instance;
ILogger? origLogger = Logging.Logger;
_logsSb = new ();
_driver = driver;
_netInput = new (_cts.Token);
_winInput = new (_cts.Token);
_output.Size = new (width, height);
_fakeSizeMonitor = new ();
IComponentFactory cf = driver == TestDriver.DotNet
? new FakeNetComponentFactory (_netInput, _output, _fakeSizeMonitor)
: (IComponentFactory)new FakeWindowsComponentFactory (_winInput, _output, _fakeSizeMonitor);
var impl = new ApplicationImpl (cf);
var booting = new SemaphoreSlim (0, 1);
// Start the application in a background thread
_runTask = Task.Run (
() =>
{
try
{
ApplicationImpl.ChangeInstance (impl);
ILogger logger = LoggerFactory.Create (
builder =>
builder.SetMinimumLevel (LogLevel.Trace)
.AddProvider (
new TextWriterLoggerProvider (
new ThreadSafeStringWriter (_logsSb, _logsLock))))
.CreateLogger ("Test Logging");
Logging.Logger = logger;
impl.Init (null, GetDriverName ());
booting.Release ();
Toplevel t = topLevelBuilder ();
t.Closed += (s, e) => { _finished = true; };
Application.Run (t); // This will block, but it's on a background thread now
t.Dispose ();
Application.Shutdown ();
_cts.Cancel ();
}
catch (OperationCanceledException)
{ }
catch (Exception ex)
{
_ex = ex;
if (logWriter != null)
{
WriteOutLogs (logWriter);
}
_hardStop.Cancel ();
}
finally
{
ApplicationImpl.ChangeInstance (origApp);
Logging.Logger = origLogger;
_finished = true;
Application.MaximumIterationsPerSecond = Application.DefaultMaximumIterationsPerSecond;
}
},
_cts.Token);
// Wait for booting to complete with a timeout to avoid hangs
if (!booting.WaitAsync (TimeSpan.FromSeconds (10)).Result)
{
throw new TimeoutException ("Application failed to start within the allotted time.");
}
ResizeConsole (width, height);
if (_ex != null)
{
throw new ("Application crashed", _ex);
}
}
private string GetDriverName ()
{
return _driver switch
{
TestDriver.Windows => "windows",
TestDriver.DotNet => "dotnet",
_ =>
throw new ArgumentOutOfRangeException ()
};
}
/// <summary>
/// Stops the application and waits for the background thread to exit.
/// </summary>
public GuiTestContext Stop ()
{
if (_runTask.IsCompleted)
{
return this;
}
WaitIteration (() => { 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 ();
// App is having trouble shutting down, try sending some more shutdown stuff from this thread.
// If this doesn't work there will be test cascade failures as the main loop continues to run during next test.
try
{
Application.RequestStop ();
Application.Shutdown ();
}
catch (Exception)
{
throw new TimeoutException ("Application failed to stop within the allotted time.", _ex);
}
throw new TimeoutException ("Application failed to stop within the allotted time.", _ex);
}
_cts.Cancel ();
if (_ex != null)
{
throw _ex; // Propagate any exception that happened in the background task
}
return this;
}
/// <summary>
/// Hard stops the application and waits for the background thread to exit.
/// </summary>
public void HardStop (Exception? ex = null)
{
if (ex != null)
{
_ex = ex;
}
_hardStop.Cancel ();
Stop ();
}
/// <summary>
/// Cleanup to avoid state bleed between tests
/// </summary>
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",
_ex);
}
_hardStop.Cancel ();
}
/// <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;
}
/// <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)
{
return WaitIteration (
() =>
{
_output.Size = new (width, height);
_fakeSizeMonitor.RaiseSizeChanging (_output.Size);
var d = (IConsoleDriverFacade)Application.Driver!;
d.OutputBuffer.SetWindowSize (width, height);
});
}
public GuiTestContext ScreenShot (string title, TextWriter writer)
{
return WaitIteration (
() =>
{
writer.WriteLine (title + ":");
var text = Application.ToString ();
writer.WriteLine (text);
});
}
/// <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)
{
lock (_logsLock)
{
writer.WriteLine (_logsSb.ToString ());
}
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.
/// </summary>
/// <param name="a"></param>
/// <returns></returns>
public GuiTestContext WaitIteration (Action? a = null)
{
// If application has already exited don't wait!
if (_finished || _cts.Token.IsCancellationRequested || _hardStop.Token.IsCancellationRequested)
{
return this;
}
if (Thread.CurrentThread.ManagedThreadId == Application.MainThreadId)
{
throw new NotSupportedException ("Cannot WaitIteration during Invoke");
}
a ??= () => { };
var ctsLocal = new CancellationTokenSource ();
Application.Invoke (
() =>
{
try
{
a ();
ctsLocal.Cancel ();
}
catch (Exception e)
{
_ex = e;
_hardStop.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;
}
/// <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)
{
try
{
WaitIteration (doAction);
}
catch (Exception ex)
{
_ex = ex;
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.
/// </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); }
/// <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<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
{
T v;
var screen = Point.Empty;
GuiTestContext ctx = WaitIteration (
() =>
{
v = Find (evaluator);
screen = v.ViewportToScreen (new Point (0, 0));
});
Click (btn, screen.X, screen.Y);
return ctx;
}
private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY)
{
switch (_driver)
{
case TestDriver.Windows:
_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)
}
});
return WaitUntil (() => _winInput.InputBuffer.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 ();
default:
throw new ArgumentOutOfRangeException ();
}
}
private GuiTestContext WaitUntil (Func<bool> condition)
{
GuiTestContext? c = null;
var sw = Stopwatch.StartNew ();
while (!condition ())
{
if (sw.Elapsed > With.Timeout)
{
throw new TimeoutException ("Failed to reach condition within the time limit");
}
c = WaitIteration ();
}
return c ?? this;
}
public GuiTestContext Down ()
{
switch (_driver)
{
case TestDriver.Windows:
SendWindowsKey (ConsoleKeyMapping.VK.DOWN);
break;
case TestDriver.DotNet:
foreach (ConsoleKeyInfo k in NetSequences.Down)
{
SendNetKey (k);
}
break;
default:
throw new ArgumentOutOfRangeException ();
}
return WaitIteration ();
}
/// <summary>
/// Simulates the Right cursor key
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public GuiTestContext Right ()
{
switch (_driver)
{
case TestDriver.Windows:
SendWindowsKey (ConsoleKeyMapping.VK.RIGHT);
break;
case TestDriver.DotNet:
foreach (ConsoleKeyInfo k in NetSequences.Right)
{
SendNetKey (k);
}
WaitIteration ();
break;
default:
throw new ArgumentOutOfRangeException ();
}
return WaitIteration ();
}
/// <summary>
/// Simulates the Left cursor key
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public GuiTestContext Left ()
{
switch (_driver)
{
case TestDriver.Windows:
SendWindowsKey (ConsoleKeyMapping.VK.LEFT);
break;
case TestDriver.DotNet:
foreach (ConsoleKeyInfo k in NetSequences.Left)
{
SendNetKey (k);
}
break;
default:
throw new ArgumentOutOfRangeException ();
}
return WaitIteration ();
}
/// <summary>
/// Simulates the up cursor key
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public GuiTestContext Up ()
{
switch (_driver)
{
case TestDriver.Windows:
SendWindowsKey (ConsoleKeyMapping.VK.UP);
break;
case TestDriver.DotNet:
foreach (ConsoleKeyInfo k in NetSequences.Up)
{
SendNetKey (k);
}
break;
default:
throw new ArgumentOutOfRangeException ();
}
return WaitIteration ();
}
/// <summary>
/// Simulates pressing the Return/Enter (newline) key.
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public GuiTestContext Enter ()
{
switch (_driver)
{
case TestDriver.Windows:
SendWindowsKey (
new WindowsConsole.KeyEventRecord
{
UnicodeChar = '\r',
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
wRepeatCount = 1,
wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN,
wVirtualScanCode = 28
});
break;
case TestDriver.DotNet:
SendNetKey (new ('\r', ConsoleKey.Enter, false, false, false));
break;
default:
throw new ArgumentOutOfRangeException ();
}
return WaitIteration ();
}
/// <summary>
/// Simulates pressing the Esc (Escape) key.
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public GuiTestContext Escape ()
{
switch (_driver)
{
case TestDriver.Windows:
SendWindowsKey (
new WindowsConsole.KeyEventRecord
{
UnicodeChar = '\u001b',
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
wRepeatCount = 1,
wVirtualKeyCode = ConsoleKeyMapping.VK.ESCAPE,
wVirtualScanCode = 1
});
break;
case TestDriver.DotNet:
// 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 ();
}
return this;
}
/// <summary>
/// Simulates pressing the Tab key.
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public GuiTestContext Tab ()
{
switch (_driver)
{
case TestDriver.Windows:
SendWindowsKey (
new WindowsConsole.KeyEventRecord
{
UnicodeChar = '\t',
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
wRepeatCount = 1,
wVirtualKeyCode = 0,
wVirtualScanCode = 0
});
break;
case TestDriver.DotNet:
// 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 ();
}
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="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;
}
/// <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>
/// Send a full windows OS key including both down and up.
/// </summary>
/// <param name="fullKey"></param>
private void SendWindowsKey (WindowsConsole.KeyEventRecord fullKey)
{
WindowsConsole.KeyEventRecord down = fullKey;
WindowsConsole.KeyEventRecord up = fullKey; // because struct this is new copy
down.bKeyDown = true;
up.bKeyDown = false;
_winInput.InputBuffer!.Enqueue (
new ()
{
EventType = WindowsConsole.EventType.Key,
KeyEvent = down
});
_winInput.InputBuffer.Enqueue (
new ()
{
EventType = WindowsConsole.EventType.Key,
KeyEvent = up
});
WaitIteration ();
}
private void SendNetKey (ConsoleKeyInfo consoleKeyInfo, bool wait = true)
{
_netInput.InputBuffer!.Enqueue (consoleKeyInfo);
if (wait)
{
WaitUntil (() => _netInput.InputBuffer.IsEmpty);
}
}
/// <summary>
/// Sends a special key e.g. cursor key that does not map to a specific character
/// </summary>
/// <param name="specialKey"></param>
private void SendWindowsKey (ConsoleKeyMapping.VK specialKey)
{
_winInput.InputBuffer!.Enqueue (
new ()
{
EventType = WindowsConsole.EventType.Key,
KeyEvent = new ()
{
bKeyDown = true,
wRepeatCount = 0,
wVirtualKeyCode = specialKey,
wVirtualScanCode = 0,
UnicodeChar = '\0',
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
}
});
_winInput.InputBuffer.Enqueue (
new ()
{
EventType = WindowsConsole.EventType.Key,
KeyEvent = new ()
{
bKeyDown = false,
wRepeatCount = 0,
wVirtualKeyCode = specialKey,
wVirtualScanCode = 0,
UnicodeChar = '\0',
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
}
});
WaitIteration ();
}
/// <summary>
/// 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)
{
WaitIteration (() => 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>
/// <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)
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"
+ 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); }
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;
}
private void Fail (string reason)
{
Stop ();
throw new (reason);
}
public GuiTestContext Send (Key key)
{
return WaitIteration (
() =>
{
if (Application.Driver is IConsoleDriverFacade facade)
{
facade.InputProcessor.OnKeyDown (key);
facade.InputProcessor.OnKeyUp (key);
}
else
{
Fail ("Expected Application.Driver to be IConsoleDriverFacade");
}
});
}
/// <summary>
/// Returns the last set position of the cursor.
/// </summary>
/// <returns></returns>
public Point GetCursorPosition () { return _output.CursorPosition; }
}
internal class FakeWindowsComponentFactory : WindowsComponentFactory
{
private readonly FakeWindowsInput _winInput;
private readonly FakeOutput _output;
private readonly FakeSizeMonitor _fakeSizeMonitor;
public FakeWindowsComponentFactory (FakeWindowsInput winInput, FakeOutput output, FakeSizeMonitor fakeSizeMonitor)
{
_winInput = winInput;
_output = output;
_fakeSizeMonitor = fakeSizeMonitor;
}
/// <inheritdoc/>
public override IConsoleInput<WindowsConsole.InputRecord> CreateInput () { return _winInput; }
/// <inheritdoc/>
public override IConsoleOutput CreateOutput () { return _output; }
/// <inheritdoc/>
public override IWindowSizeMonitor CreateWindowSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer) { return _fakeSizeMonitor; }
}
internal class FakeNetComponentFactory : NetComponentFactory
{
private readonly FakeNetInput _netInput;
private readonly FakeOutput _output;
private readonly FakeSizeMonitor _fakeSizeMonitor;
public FakeNetComponentFactory (FakeNetInput netInput, FakeOutput output, FakeSizeMonitor fakeSizeMonitor)
{
_netInput = netInput;
_output = output;
_fakeSizeMonitor = fakeSizeMonitor;
}
/// <inheritdoc/>
public override IConsoleInput<ConsoleKeyInfo> CreateInput () { return _netInput; }
/// <inheritdoc/>
public override IConsoleOutput CreateOutput () { return _output; }
/// <inheritdoc/>
public override IWindowSizeMonitor CreateWindowSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer) { return _fakeSizeMonitor; }
}