mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
* Consider width2 chars that are not IsBmp
* Apply same fix in WindowsDriver
* Explicitly use type of local variable
* Revert changes to WindowsDriver
* Assume we are running in a terminal that supports true color by default unless user explicitly forces 16
* Switch to SetAttribute and WriteConsole instead of WriteConsoleOutput for 16 color mode
* Fix some cursor issues (WIP)
* Remove concept of 'dirty rows' from v2 as its never actually used
* Remove damageRegion as it does nothing
* Make string builder to console writing simpler
* Radically simplify Write method
* Simplify conditional logic
* Simplify restoring cursor position
* Reference local variable for console buffer
* Reduce calls to ConsoleWrite by accumulating till attribute changes
* When resizing v2 16 color mode on windows, recreate the back buffer to match its size
* Fixes for VTS enabled
* Fix _lastSize never being assigned
* Fixes VTS for Force16Colors
* Fixes force16Colors in VTS
* Fixes escape sequences always echoing in non-VTS
* Force Force16Colors in non-VTS. It have a bug in adding a newline in the last line
* WIP Add base class for NetOutput
* Abstract away how we change attribute
* WIP - Make WindowsOutput use base class
* WIP working to fix set cursor position
* Remove commented out code
* Fixes legacy output mode
* Fixes size with no alt buffer supported on VTS and size restore after maximized.
* Fix set cursor which also fixes the broken surrogate pairs
* Add force parameter
* Fixes an issue that only happens with Windows Terminal when paste surrogate pairs by press Ctrl+V
* In Windows escape sequences must be sent during the lifetime of the console which is created in input handle
* Ensure flush the input buffer before reset the console
* Flush input buffer before reset console in v2win
* Fixes issue in v2net not being refreshing the menu bar at start
* Only force layout and draw on size changed.
* Fix v2net issue not draw first line by forcing set cursor position
* Set _lastCursorPosition nullable and remove bool force from set cursor position
* Remove force parameter
* Add v2 version of fake driver attribute
* Make direct replacement and wire up window resizing events
* Update casts to use V2 fake driver instead
* Adjust interfaces to expose less internals
* Fix not raising iteration event in v2
* WIP investigate what it takes to do resize and redraw using TextAlignment_Centered as example
* Sketch adding component factory
* Create relevant fake component factories
* Add window size monitor into factory
* Fake size monitor injecting
* Add helper for faking console resize in AutoInitShutdown tests
* Fix size setting in FakeDriverV2
* Switch to new method
* Fix IsLegacy becoming false when using blank constructor
* Fix for Ready not being raised when showing same top twice also fixes garbage collection issue if running millions of top levels
* Fix tests
* Remove auto init
* Restore conditional compilation stuff
* Restore 'if running unit tests' logic
* Check only for the output being specific classes for the suppression
* Fix ShadowView blowing up with index out of bounds error
* Fix resize in fluent tests
* Fix for people using Iteration call directly
* Fix more calls to iteration to use
AutoInitShutdownAttribute.RunIteration ();
* Add comment
* Remove assumption that Run with prior view not disposed should throw
* Fix timings in Dialog_Opened_From_Another_Dialog
* Fix Zero_Buttons_Works
* Standardize and fix Button_IsDefault_True_Return_His_Index_On_Accepting
* Fix iteration counts on MessageBoxTests
* Fix WizartTests and DrawTests_Ruler
* Implement SendKeys into ConsoleDriverFacade
* Fix SendKeys in console driver facade such that FileDialogTests works
Fix when Clip is null in popover
* Add missing dispose call to test
* Fix support for Esc in facade SendKeys
* Fix AutocompleteTests
* Fix various tests
* Replace LayoutAndDraw with run iteration
* Fix draw issues
* fix draw order
* Fix run iteration calls
* Fix unit tests
* Fix SendKeys in facade.
* Manipulate upper and lower cases.
* Add IsValidInput method to the interface.
* Fix SendKeys scenario
* Fixes surrogate pairs in the label
* Make tests more sensible - they are testing draw functionality. Callbacks do not need to happen in Iteration method
* Fix tests and harden cleanup in AutoInitShutdownAttribute v2 lifecycle dispose
* Delete extra create input call
* Fix mocks and order of exceptions thrown in Run when things are not initialized
* Revert use of `MapConsoleKeyInfoToKeyCode`
* Ignore casing as it is not what test is really about
* Clear application top and top levels before each auto init shutdown test
* Fix for unstable tests
* Restore actually working SendKeys code
* option to pass logger in fluent ctor
* restore ToArray
* Fix SendKeys method and add extension to unit test
* Leverage the EscSeqUtils.MapConsoleKeyInfo method to avoid duplicate code
* Remove unnecessary hack
* Using only KeyCode for rKeys
* Recover modifier keys in surrogate pairs
* Reformat
* Remove iteration limit for benchmarking in v2
* remove iteration delay to identify bugs
* Remove nudge to unique key and make Then run on UI thread
* fix fluid assertions
* Ensure UI operations all happen on UI thread
* Add explicit error for WaitIteration during an invoke
* Remove timeout added for debug
* Catch failing asserts better
* Fix screenshot
* Fix null ref
* Fix race condition in processing input
* Test fixing
* Standardize asserts
* Remove calls to layout and draw, remove pointless lock and enable reading Cancelled from Dialog even if it is disposed
* fix bad merge
* Make logs access threadsafe
* add extra wait to remove race between iteration end and assert
* Code cleanup
* Remove test for crash on access Cancelled after dispose as this is no longer a restriction
* Change resize console to run on UI thread - fixing race condition with redrawing
* Restore original frame rate after test
* Restore nudge to unique key
* Code Cleanup
* Fix for cascading failures when an assert fails in a specific test
* fix for bad merge
* Address PR feedback
* Move classes to seperate files and add xmldoc
* xml doc warnings
* More xml comments docs
* Fix spelling
---------
Co-authored-by: BDisp <bd.bdisp@gmail.com>
This commit is contained in:
158
Tests/TerminalGuiFluentTesting/FakeDriverV2.cs
Normal file
158
Tests/TerminalGuiFluentTesting/FakeDriverV2.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Drawing;
|
||||
using TerminalGuiFluentTesting;
|
||||
|
||||
namespace Terminal.Gui.Drivers;
|
||||
|
||||
public class FakeApplicationFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an initialized fake application which will be cleaned up when result object
|
||||
/// is disposed.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IDisposable SetupFakeApplication ()
|
||||
{
|
||||
var cts = new CancellationTokenSource ();
|
||||
var fakeInput = new FakeNetInput (cts.Token);
|
||||
FakeOutput _output = new ();
|
||||
_output.Size = new (25, 25);
|
||||
|
||||
|
||||
IApplication origApp = ApplicationImpl.Instance;
|
||||
|
||||
var sizeMonitor = new FakeSizeMonitor ();
|
||||
|
||||
var v2 = new ApplicationV2 (new FakeNetComponentFactory (fakeInput, _output, sizeMonitor));
|
||||
|
||||
ApplicationImpl.ChangeInstance (v2);
|
||||
v2.Init (null,"v2net");
|
||||
|
||||
var d = (ConsoleDriverFacade<ConsoleKeyInfo>)Application.Driver;
|
||||
sizeMonitor.SizeChanging += (_, e) =>
|
||||
{
|
||||
if (e.Size != null)
|
||||
{
|
||||
var s = e.Size.Value;
|
||||
_output.Size = s;
|
||||
d.OutputBuffer.SetWindowSize (s.Width, s.Height);
|
||||
}
|
||||
};
|
||||
|
||||
return new FakeApplicationLifecycle (origApp,cts);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeApplicationLifecycle : IDisposable
|
||||
{
|
||||
private readonly IApplication _origApp;
|
||||
private readonly CancellationTokenSource _hardStop;
|
||||
|
||||
public FakeApplicationLifecycle (IApplication origApp, CancellationTokenSource hardStop)
|
||||
{
|
||||
_origApp = origApp;
|
||||
_hardStop = hardStop;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public void Dispose ()
|
||||
{
|
||||
_hardStop.Cancel();
|
||||
|
||||
Application.Top?.Dispose ();
|
||||
Application.Shutdown ();
|
||||
ApplicationImpl.ChangeInstance (_origApp);
|
||||
}
|
||||
}
|
||||
|
||||
public class FakeDriverFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="FakeDriverV2"/> using default options
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IFakeDriverV2 Create ()
|
||||
{
|
||||
return new FakeDriverV2 (
|
||||
new ConcurrentQueue<ConsoleKeyInfo> (),
|
||||
new OutputBuffer (),
|
||||
new FakeOutput (),
|
||||
() => DateTime.Now,
|
||||
new FakeSizeMonitor ());
|
||||
}
|
||||
}
|
||||
|
||||
public interface IFakeDriverV2 : IConsoleDriver, IConsoleDriverFacade
|
||||
{
|
||||
void SetBufferSize (int width, int height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IConsoleDriver"/> that uses fake input/output.
|
||||
/// This is a lightweight alternative to <see cref="GuiTestContext"/> (if you don't
|
||||
/// need the entire application main loop running).
|
||||
/// </summary>
|
||||
class FakeDriverV2 : ConsoleDriverFacade<ConsoleKeyInfo>, IFakeDriverV2
|
||||
{
|
||||
public ConcurrentQueue<ConsoleKeyInfo> InputBuffer { get; }
|
||||
public FakeSizeMonitor SizeMonitor { get; }
|
||||
public OutputBuffer OutputBuffer { get; }
|
||||
|
||||
public IConsoleOutput ConsoleOutput { get; }
|
||||
|
||||
private FakeOutput _fakeOutput;
|
||||
|
||||
internal FakeDriverV2 (
|
||||
ConcurrentQueue<ConsoleKeyInfo> inputBuffer,
|
||||
OutputBuffer outputBuffer,
|
||||
FakeOutput fakeOutput,
|
||||
Func<DateTime> datetimeFunc,
|
||||
FakeSizeMonitor sizeMonitor) :
|
||||
base (new NetInputProcessor (inputBuffer),
|
||||
outputBuffer,
|
||||
fakeOutput,
|
||||
new (new AnsiResponseParser (), datetimeFunc),
|
||||
sizeMonitor)
|
||||
{
|
||||
InputBuffer = inputBuffer;
|
||||
SizeMonitor = sizeMonitor;
|
||||
OutputBuffer = outputBuffer;
|
||||
ConsoleOutput = _fakeOutput = fakeOutput;
|
||||
SizeChanged += (_, e) =>
|
||||
{
|
||||
if (e.Size != null)
|
||||
{
|
||||
var s = e.Size.Value;
|
||||
_fakeOutput.Size = s;
|
||||
OutputBuffer.SetWindowSize (s.Width,s.Height);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public void SetBufferSize (int width, int height)
|
||||
{
|
||||
SizeMonitor.RaiseSizeChanging (new Size (width,height));
|
||||
OutputBuffer.SetWindowSize (width,height);
|
||||
}
|
||||
}
|
||||
|
||||
public class FakeSizeMonitor : IWindowSizeMonitor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<SizeChangedEventArgs>? SizeChanging;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Poll ()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="SizeChanging"/> event.
|
||||
/// </summary>
|
||||
/// <param name="newSize"></param>
|
||||
public void RaiseSizeChanging (Size newSize)
|
||||
{
|
||||
SizeChanging?.Invoke (this,new (newSize));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Drawing;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -13,106 +14,105 @@ public class GuiTestContext : IDisposable
|
||||
private readonly CancellationTokenSource _cts = new ();
|
||||
private readonly CancellationTokenSource _hardStop = new (With.Timeout);
|
||||
private readonly Task _runTask;
|
||||
private Exception _ex;
|
||||
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 V2TestDriver _driver;
|
||||
private bool _finished;
|
||||
private readonly object _threadLock = new ();
|
||||
private readonly FakeSizeMonitor _fakeSizeMonitor;
|
||||
|
||||
internal GuiTestContext (Func<Toplevel> topLevelBuilder, int width, int height, V2TestDriver driver)
|
||||
internal GuiTestContext (Func<Toplevel> topLevelBuilder, int width, int height, V2TestDriver driver, TextWriter? logWriter = null)
|
||||
{
|
||||
lock (_threadLock)
|
||||
{
|
||||
IApplication origApp = ApplicationImpl.Instance;
|
||||
ILogger? origLogger = Logging.Logger;
|
||||
_logsSb = new ();
|
||||
_driver = driver;
|
||||
// Remove frame limit
|
||||
Application.MaximumIterationsPerSecond = ushort.MaxValue;
|
||||
|
||||
_netInput = new (_cts.Token);
|
||||
_winInput = new (_cts.Token);
|
||||
IApplication origApp = ApplicationImpl.Instance;
|
||||
ILogger? origLogger = Logging.Logger;
|
||||
_logsSb = new ();
|
||||
_driver = driver;
|
||||
|
||||
_output.Size = new (width, height);
|
||||
_netInput = new (_cts.Token);
|
||||
_winInput = new (_cts.Token);
|
||||
|
||||
var v2 = new ApplicationV2 (
|
||||
() => _netInput,
|
||||
() => _output,
|
||||
() => _winInput,
|
||||
() => _output);
|
||||
_output.Size = new (width, height);
|
||||
_fakeSizeMonitor = new ();
|
||||
|
||||
var booting = new SemaphoreSlim (0, 1);
|
||||
IComponentFactory cf = driver == V2TestDriver.V2Net
|
||||
? new FakeNetComponentFactory (_netInput, _output, _fakeSizeMonitor)
|
||||
: (IComponentFactory)new FakeWindowsComponentFactory (_winInput, _output, _fakeSizeMonitor);
|
||||
|
||||
// Start the application in a background thread
|
||||
_runTask = Task.Run (() =>
|
||||
var v2 = new ApplicationV2 (cf);
|
||||
|
||||
var booting = new SemaphoreSlim (0, 1);
|
||||
|
||||
// Start the application in a background thread
|
||||
_runTask = Task.Run (
|
||||
() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (Application.Top is { })
|
||||
ApplicationImpl.ChangeInstance (v2);
|
||||
|
||||
ILogger logger = LoggerFactory.Create (
|
||||
builder =>
|
||||
builder.SetMinimumLevel (LogLevel.Trace)
|
||||
.AddProvider (
|
||||
new TextWriterLoggerProvider (
|
||||
new ThreadSafeStringWriter (_logsSb, _logsLock))))
|
||||
.CreateLogger ("Test Logging");
|
||||
Logging.Logger = logger;
|
||||
|
||||
v2.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)
|
||||
{
|
||||
Task.Delay (300).Wait ();
|
||||
WriteOutLogs (logWriter);
|
||||
}
|
||||
})
|
||||
.ContinueWith (
|
||||
(task, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
_ex = task.Exception ?? new Exception ("Unknown error in background task");
|
||||
}
|
||||
|
||||
// Ensure we are not running on the main thread
|
||||
if (ApplicationImpl.Instance != origApp)
|
||||
{
|
||||
throw new InvalidOperationException (
|
||||
"Application instance is not the original one, this should not happen.");
|
||||
}
|
||||
_hardStop.Cancel ();
|
||||
}
|
||||
finally
|
||||
{
|
||||
ApplicationImpl.ChangeInstance (origApp);
|
||||
Logging.Logger = origLogger;
|
||||
_finished = true;
|
||||
|
||||
ApplicationImpl.ChangeInstance (v2);
|
||||
Application.MaximumIterationsPerSecond = Application.DefaultMaximumIterationsPerSecond;
|
||||
}
|
||||
},
|
||||
_cts.Token);
|
||||
|
||||
ILogger logger = LoggerFactory.Create (builder =>
|
||||
builder.SetMinimumLevel (LogLevel.Trace)
|
||||
.AddProvider (
|
||||
new TextWriterLoggerProvider (
|
||||
new StringWriter (_logsSb))))
|
||||
.CreateLogger ("Test Logging");
|
||||
Logging.Logger = logger;
|
||||
// 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.");
|
||||
}
|
||||
|
||||
v2.Init (null, GetDriverName ());
|
||||
ResizeConsole (width, height);
|
||||
|
||||
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 ();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_ex = ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ApplicationImpl.ChangeInstance (origApp);
|
||||
Logging.Logger = origLogger;
|
||||
_finished = true;
|
||||
}
|
||||
},
|
||||
_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.");
|
||||
}
|
||||
|
||||
WaitIteration ();
|
||||
if (_ex != null)
|
||||
{
|
||||
throw new ("Application crashed", _ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ public class GuiTestContext : IDisposable
|
||||
return this;
|
||||
}
|
||||
|
||||
Application.Invoke (() => { Application.RequestStop (); });
|
||||
WaitIteration (() => { Application.RequestStop (); });
|
||||
|
||||
// Wait for the application to stop, but give it a 1-second timeout
|
||||
if (!_runTask.Wait (TimeSpan.FromMilliseconds (1000)))
|
||||
@@ -147,7 +147,19 @@ public class GuiTestContext : IDisposable
|
||||
// Timeout occurred, force the task to stop
|
||||
_hardStop.Cancel ();
|
||||
|
||||
throw new TimeoutException ("Application failed to stop within the allotted time.");
|
||||
// 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 ();
|
||||
@@ -163,8 +175,13 @@ public class GuiTestContext : IDisposable
|
||||
/// <summary>
|
||||
/// Hard stops the application and waits for the background thread to exit.
|
||||
/// </summary>
|
||||
public void HardStop ()
|
||||
public void HardStop (Exception? ex = null)
|
||||
{
|
||||
if (ex != null)
|
||||
{
|
||||
_ex = ex;
|
||||
}
|
||||
|
||||
_hardStop.Cancel ();
|
||||
Stop ();
|
||||
}
|
||||
@@ -179,7 +196,8 @@ public class GuiTestContext : IDisposable
|
||||
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");
|
||||
"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 ();
|
||||
@@ -213,19 +231,27 @@ public class GuiTestContext : IDisposable
|
||||
/// <returns></returns>
|
||||
public GuiTestContext ResizeConsole (int width, int height)
|
||||
{
|
||||
_output.Size = new (width, height);
|
||||
return WaitIteration (
|
||||
() =>
|
||||
{
|
||||
_output.Size = new (width, height);
|
||||
_fakeSizeMonitor.RaiseSizeChanging (_output.Size);
|
||||
|
||||
return WaitIteration ();
|
||||
var d = (IConsoleDriverFacade)Application.Driver!;
|
||||
d.OutputBuffer.SetWindowSize (width, height);
|
||||
});
|
||||
}
|
||||
|
||||
public GuiTestContext ScreenShot (string title, TextWriter writer)
|
||||
{
|
||||
writer.WriteLine (title + ":");
|
||||
var text = Application.ToString ();
|
||||
return WaitIteration (
|
||||
() =>
|
||||
{
|
||||
writer.WriteLine (title + ":");
|
||||
var text = Application.ToString ();
|
||||
|
||||
writer.WriteLine (text);
|
||||
|
||||
return this; //WaitIteration();
|
||||
writer.WriteLine (text);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -235,7 +261,10 @@ public class GuiTestContext : IDisposable
|
||||
/// <returns></returns>
|
||||
public GuiTestContext WriteOutLogs (TextWriter writer)
|
||||
{
|
||||
writer.WriteLine (_logsSb.ToString ());
|
||||
lock (_logsLock)
|
||||
{
|
||||
writer.WriteLine (_logsSb.ToString ());
|
||||
}
|
||||
|
||||
return this; //WaitIteration();
|
||||
}
|
||||
@@ -254,14 +283,27 @@ public class GuiTestContext : IDisposable
|
||||
return this;
|
||||
}
|
||||
|
||||
if (Thread.CurrentThread.ManagedThreadId == Application.MainThreadId)
|
||||
{
|
||||
throw new NotSupportedException ("Cannot WaitIteration during Invoke");
|
||||
}
|
||||
|
||||
a ??= () => { };
|
||||
var ctsLocal = new CancellationTokenSource ();
|
||||
|
||||
Application.Invoke (
|
||||
() =>
|
||||
{
|
||||
a ();
|
||||
ctsLocal.Cancel ();
|
||||
try
|
||||
{
|
||||
a ();
|
||||
ctsLocal.Cancel ();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_ex = e;
|
||||
_hardStop.Cancel ();
|
||||
}
|
||||
});
|
||||
|
||||
// Blocks until either the token or the hardStopToken is cancelled.
|
||||
@@ -286,10 +328,11 @@ public class GuiTestContext : IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
doAction ();
|
||||
WaitIteration (doAction);
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
_ex = ex;
|
||||
HardStop ();
|
||||
|
||||
throw;
|
||||
@@ -322,10 +365,19 @@ public class GuiTestContext : IDisposable
|
||||
|
||||
private GuiTestContext Click<T> (WindowsConsole.ButtonState btn, Func<T, bool> evaluator) where T : View
|
||||
{
|
||||
T v = Find (evaluator);
|
||||
Point screen = v.ViewportToScreen (new Point (0, 0));
|
||||
T v;
|
||||
var screen = Point.Empty;
|
||||
|
||||
return Click (btn, screen.X, screen.Y);
|
||||
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)
|
||||
@@ -356,6 +408,8 @@ public class GuiTestContext : IDisposable
|
||||
}
|
||||
});
|
||||
|
||||
return WaitUntil (() => _winInput.InputBuffer.IsEmpty);
|
||||
|
||||
break;
|
||||
case V2TestDriver.V2Net:
|
||||
|
||||
@@ -370,17 +424,31 @@ public class GuiTestContext : IDisposable
|
||||
|
||||
foreach (ConsoleKeyInfo k in NetSequences.Click (netButton, screenX, screenY))
|
||||
{
|
||||
SendNetKey (k);
|
||||
SendNetKey (k, false);
|
||||
}
|
||||
|
||||
break;
|
||||
return WaitIteration ();
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException ();
|
||||
}
|
||||
}
|
||||
|
||||
return WaitIteration ();
|
||||
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 ()
|
||||
@@ -648,7 +716,15 @@ public class GuiTestContext : IDisposable
|
||||
WaitIteration ();
|
||||
}
|
||||
|
||||
private void SendNetKey (ConsoleKeyInfo consoleKeyInfo) { _netInput.InputBuffer.Enqueue (consoleKeyInfo); }
|
||||
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
|
||||
@@ -697,7 +773,7 @@ public class GuiTestContext : IDisposable
|
||||
/// <returns></returns>
|
||||
public GuiTestContext RaiseKeyDownEvent (Key key)
|
||||
{
|
||||
Application.RaiseKeyDownEvent (key);
|
||||
WaitIteration (() => Application.RaiseKeyDownEvent (key));
|
||||
|
||||
return this; //WaitIteration();
|
||||
}
|
||||
@@ -728,9 +804,11 @@ public class GuiTestContext : IDisposable
|
||||
/// 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>
|
||||
/// <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
|
||||
@@ -760,11 +838,14 @@ public class GuiTestContext : IDisposable
|
||||
// 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");
|
||||
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;
|
||||
}
|
||||
@@ -773,7 +854,9 @@ public class GuiTestContext : IDisposable
|
||||
// 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");
|
||||
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;
|
||||
}
|
||||
@@ -781,6 +864,8 @@ public class GuiTestContext : IDisposable
|
||||
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;
|
||||
@@ -830,25 +915,70 @@ public class GuiTestContext : IDisposable
|
||||
|
||||
public GuiTestContext Send (Key key)
|
||||
{
|
||||
if (Application.Driver is IConsoleDriverFacade facade)
|
||||
{
|
||||
facade.InputProcessor.OnKeyDown (key);
|
||||
facade.InputProcessor.OnKeyUp (key);
|
||||
}
|
||||
else
|
||||
{
|
||||
Fail ("Expected Application.Driver to be IConsoleDriverFacade");
|
||||
}
|
||||
|
||||
return this;
|
||||
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.
|
||||
/// Returns the last set position of the cursor.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Point GetCursorPosition ()
|
||||
{
|
||||
return _output.CursorPosition;
|
||||
}
|
||||
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; }
|
||||
}
|
||||
|
||||
31
Tests/TerminalGuiFluentTesting/ThreadSafeStringWriter.cs
Normal file
31
Tests/TerminalGuiFluentTesting/ThreadSafeStringWriter.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Text;
|
||||
|
||||
namespace TerminalGuiFluentTesting;
|
||||
|
||||
class ThreadSafeStringWriter : StringWriter
|
||||
{
|
||||
private readonly object _lock;
|
||||
|
||||
public ThreadSafeStringWriter (StringBuilder sb, object syncLock) : base (sb)
|
||||
{
|
||||
_lock = syncLock;
|
||||
}
|
||||
|
||||
public override void Write (char value)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
base.Write (value);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write (string? value)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
base.Write (value);
|
||||
}
|
||||
}
|
||||
|
||||
// (override other Write* methods as needed)
|
||||
}
|
||||
@@ -12,24 +12,24 @@ public static class With
|
||||
/// <param name="width"></param>
|
||||
/// <param name="height"></param>
|
||||
/// <param name="v2TestDriver">Which v2 v2TestDriver to use for the test</param>
|
||||
/// <param name="logWriter"></param>
|
||||
/// <returns></returns>
|
||||
public static GuiTestContext A<T> (int width, int height, V2TestDriver v2TestDriver) where T : Toplevel, new ()
|
||||
public static GuiTestContext A<T> (int width, int height, V2TestDriver v2TestDriver, TextWriter? logWriter = null) where T : Toplevel, new ()
|
||||
{
|
||||
return new (() => new T (), width, height,v2TestDriver);
|
||||
return new (() => new T (), width, height,v2TestDriver,logWriter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overload that takes an existing instance <paramref name="toplevel"/>
|
||||
/// instead of creating one.
|
||||
/// Overload that takes a function to create instance <paramref name="toplevelFactory"/> after application is initialized.
|
||||
/// </summary>
|
||||
/// <param name="toplevel"></param>
|
||||
/// <param name="toplevelFactory"></param>
|
||||
/// <param name="width"></param>
|
||||
/// <param name="height"></param>
|
||||
/// <param name="v2TestDriver"></param>
|
||||
/// <returns></returns>
|
||||
public static GuiTestContext A (Toplevel toplevel, int width, int height, V2TestDriver v2TestDriver)
|
||||
public static GuiTestContext A (Func<Toplevel> toplevelFactory, int width, int height, V2TestDriver v2TestDriver)
|
||||
{
|
||||
return new (()=>toplevel, width, height, v2TestDriver);
|
||||
return new (toplevelFactory, width, height, v2TestDriver);
|
||||
}
|
||||
/// <summary>
|
||||
/// The global timeout to allow for any given application to run for before shutting down.
|
||||
|
||||
Reference in New Issue
Block a user