mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
Rename to TerminalGuiFluentTesting
This commit is contained in:
@@ -87,7 +87,7 @@
|
|||||||
<InternalsVisibleTo Include="StressTests" />
|
<InternalsVisibleTo Include="StressTests" />
|
||||||
<InternalsVisibleTo Include="IntegrationTests" />
|
<InternalsVisibleTo Include="IntegrationTests" />
|
||||||
<InternalsVisibleTo Include="TerminalGuiDesigner" />
|
<InternalsVisibleTo Include="TerminalGuiDesigner" />
|
||||||
<InternalsVisibleTo Include="TerminalGuiFluentAssertions" />
|
<InternalsVisibleTo Include="TerminalGuiFluentTesting" />
|
||||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- =================================================================== -->
|
<!-- =================================================================== -->
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StressTests", "Tests\Stress
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable", "Tests\UnitTestsParallelizable\UnitTests.Parallelizable.csproj", "{DE780834-190A-8277-51FD-750CC666E82D}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable", "Tests\UnitTestsParallelizable\UnitTests.Parallelizable.csproj", "{DE780834-190A-8277-51FD-750CC666E82D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentAssertions", "TerminalGuiFluentAssertions\TerminalGuiFluentAssertions.csproj", "{7C610F03-9E38-409F-9B21-A02D5569E16A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting", "TerminalGuiFluentAssertions\TerminalGuiFluentTesting.csproj", "{7C610F03-9E38-409F-9B21-A02D5569E16A}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
|||||||
@@ -1,404 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Drawing;
|
|
||||||
using FluentAssertions;
|
|
||||||
using FluentAssertions.Numeric;
|
|
||||||
using Terminal.Gui;
|
|
||||||
using Terminal.Gui.ConsoleDrivers;
|
|
||||||
using static Unix.Terminal.Curses;
|
|
||||||
|
|
||||||
namespace TerminalGuiFluentAssertions;
|
|
||||||
|
|
||||||
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 (new [] { token.WaitHandle, _hardStopToken.WaitHandle, _timeoutCts.Token.WaitHandle });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakeNetInput (CancellationToken hardStopToken) : FakeInput<ConsoleKeyInfo> (hardStopToken), INetInput
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakeWindowsInput (CancellationToken hardStopToken) : FakeInput<WindowsConsole.InputRecord> (hardStopToken), IWindowsInput
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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 GetWindowSize ()
|
|
||||||
{
|
|
||||||
return Size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void SetCursorVisibility (CursorVisibility visibility)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void SetCursorPosition (int col, int row)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// Entry point to fluent assertions.
|
|
||||||
/// </summary>
|
|
||||||
public static class With
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Entrypoint to fluent assertions
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="width"></param>
|
|
||||||
/// <param name="height"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static GuiTestContext<T> A<T> (int width, int height) where T : Toplevel, new ()
|
|
||||||
{
|
|
||||||
return new (width,height);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The global timeout to allow for any given application to run for before shutting down.
|
|
||||||
/// </summary>
|
|
||||||
public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds (30);
|
|
||||||
}
|
|
||||||
public class GuiTestContext<T> : IDisposable where T : Toplevel, new()
|
|
||||||
{
|
|
||||||
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 View _lastView;
|
|
||||||
|
|
||||||
internal GuiTestContext (int width, int height)
|
|
||||||
{
|
|
||||||
IApplication origApp = ApplicationImpl.Instance;
|
|
||||||
|
|
||||||
var netInput = new FakeNetInput (_cts.Token);
|
|
||||||
winInput = new FakeWindowsInput (_cts.Token);
|
|
||||||
|
|
||||||
_output.Size = new (width, height);
|
|
||||||
|
|
||||||
var v2 = new ApplicationV2(
|
|
||||||
() => netInput,
|
|
||||||
()=>_output,
|
|
||||||
() => winInput,
|
|
||||||
() => _output);
|
|
||||||
|
|
||||||
var booting = new SemaphoreSlim (0, 1);
|
|
||||||
|
|
||||||
// Start the application in a background thread
|
|
||||||
_runTask = Task.Run (() =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ApplicationImpl.ChangeInstance (v2);
|
|
||||||
|
|
||||||
v2.Init (null,"v2win");
|
|
||||||
|
|
||||||
booting.Release ();
|
|
||||||
|
|
||||||
Application.Run<T> (); // This will block, but it's on a background thread now
|
|
||||||
|
|
||||||
Application.Shutdown ();
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{ }
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_ex = ex;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ApplicationImpl.ChangeInstance (origApp);
|
|
||||||
}
|
|
||||||
}, _cts.Token);
|
|
||||||
|
|
||||||
// Wait for booting to complete with a timeout to avoid hangs
|
|
||||||
if (!booting.WaitAsync (TimeSpan.FromSeconds (5)).Result)
|
|
||||||
{
|
|
||||||
throw new TimeoutException ("Application failed to start within the allotted time.");
|
|
||||||
}
|
|
||||||
|
|
||||||
WaitIteration ();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stops the application and waits for the background thread to exit.
|
|
||||||
/// </summary>
|
|
||||||
public GuiTestContext<T> Stop ()
|
|
||||||
{
|
|
||||||
if (_runTask.IsCompleted)
|
|
||||||
{
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Application.Invoke (()=> 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 ();
|
|
||||||
throw new TimeoutException ("Application failed to stop within the allotted time.");
|
|
||||||
}
|
|
||||||
_cts.Cancel ();
|
|
||||||
|
|
||||||
if (_ex != null)
|
|
||||||
{
|
|
||||||
throw _ex; // Propagate any exception that happened in the background task
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup to avoid state bleed between tests
|
|
||||||
public void Dispose ()
|
|
||||||
{
|
|
||||||
Stop ();
|
|
||||||
|
|
||||||
if (_hardStop.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
throw new Exception (
|
|
||||||
"Application was hard stopped, typically this means it timed out or did not shutdown gracefully. Ensure you call Stop in your test");
|
|
||||||
}
|
|
||||||
_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<T> Add (View v)
|
|
||||||
{
|
|
||||||
WaitIteration (
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
var top = Application.Top ?? throw new Exception("Top was null so could not add view");
|
|
||||||
top.Add (v);
|
|
||||||
top.Layout ();
|
|
||||||
_lastView = v;
|
|
||||||
});
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GuiTestContext<T> ResizeConsole (int width, int height)
|
|
||||||
{
|
|
||||||
_output.Size = new Size (width,height);
|
|
||||||
|
|
||||||
return WaitIteration ();
|
|
||||||
}
|
|
||||||
public GuiTestContext<T> ScreenShot (string title, TextWriter writer)
|
|
||||||
{
|
|
||||||
writer.WriteLine(title +":");
|
|
||||||
var text = Application.ToString ();
|
|
||||||
|
|
||||||
writer.WriteLine(text);
|
|
||||||
|
|
||||||
return WaitIteration ();
|
|
||||||
}
|
|
||||||
public GuiTestContext<T> WaitIteration (Action? a = null)
|
|
||||||
{
|
|
||||||
a ??= () => { };
|
|
||||||
var ctsLocal = new CancellationTokenSource ();
|
|
||||||
|
|
||||||
|
|
||||||
Application.Invoke (()=>
|
|
||||||
{
|
|
||||||
a();
|
|
||||||
ctsLocal.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GuiTestContext<T> Assert<T2> (AndConstraint<T2> be)
|
|
||||||
{
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GuiTestContext<T> RightClick (int screenX, int screenY)
|
|
||||||
{
|
|
||||||
return Click (WindowsConsole.ButtonState.Button3Pressed,screenX, screenY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public GuiTestContext<T> LeftClick (int screenX, int screenY)
|
|
||||||
{
|
|
||||||
return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private GuiTestContext<T> Click (WindowsConsole.ButtonState btn, int screenX, int screenY)
|
|
||||||
{
|
|
||||||
winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord ()
|
|
||||||
{
|
|
||||||
EventType = WindowsConsole.EventType.Mouse,
|
|
||||||
MouseEvent = new WindowsConsole.MouseEventRecord ()
|
|
||||||
{
|
|
||||||
ButtonState = btn,
|
|
||||||
MousePosition = new WindowsConsole.Coord ((short)screenX, (short)screenY)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord ()
|
|
||||||
{
|
|
||||||
EventType = WindowsConsole.EventType.Mouse,
|
|
||||||
MouseEvent = new WindowsConsole.MouseEventRecord ()
|
|
||||||
{
|
|
||||||
ButtonState = WindowsConsole.ButtonState.NoButtonPressed,
|
|
||||||
MousePosition = new WindowsConsole.Coord ((short)screenX, (short)screenY)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
WaitIteration ();
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GuiTestContext<T> Down ()
|
|
||||||
{
|
|
||||||
winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord ()
|
|
||||||
{
|
|
||||||
EventType = WindowsConsole.EventType.Key,
|
|
||||||
KeyEvent = new WindowsConsole.KeyEventRecord
|
|
||||||
{
|
|
||||||
bKeyDown = true,
|
|
||||||
wRepeatCount = 0,
|
|
||||||
wVirtualKeyCode = ConsoleKeyMapping.VK.DOWN,
|
|
||||||
wVirtualScanCode = 0,
|
|
||||||
UnicodeChar = '\0',
|
|
||||||
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord ()
|
|
||||||
{
|
|
||||||
EventType = WindowsConsole.EventType.Key,
|
|
||||||
KeyEvent = new WindowsConsole.KeyEventRecord
|
|
||||||
{
|
|
||||||
bKeyDown = false,
|
|
||||||
wRepeatCount = 0,
|
|
||||||
wVirtualKeyCode = ConsoleKeyMapping.VK.DOWN,
|
|
||||||
wVirtualScanCode = 0,
|
|
||||||
UnicodeChar = '\0',
|
|
||||||
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
WaitIteration ();
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
public GuiTestContext<T> Enter ()
|
|
||||||
{
|
|
||||||
winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord ()
|
|
||||||
{
|
|
||||||
EventType = WindowsConsole.EventType.Key,
|
|
||||||
KeyEvent = new WindowsConsole.KeyEventRecord
|
|
||||||
{
|
|
||||||
bKeyDown = true,
|
|
||||||
wRepeatCount = 0,
|
|
||||||
wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN,
|
|
||||||
wVirtualScanCode = 0,
|
|
||||||
UnicodeChar = '\0',
|
|
||||||
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord ()
|
|
||||||
{
|
|
||||||
EventType = WindowsConsole.EventType.Key,
|
|
||||||
KeyEvent = new WindowsConsole.KeyEventRecord
|
|
||||||
{
|
|
||||||
bKeyDown = false,
|
|
||||||
wRepeatCount = 0,
|
|
||||||
wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN,
|
|
||||||
wVirtualScanCode = 0,
|
|
||||||
UnicodeChar = '\0',
|
|
||||||
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
WaitIteration ();
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
public GuiTestContext<T> 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 Exception ("Could not determine which view to add to");
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
59
TerminalGuiFluentAssertions/ClassDiagram1.cd
Normal file
59
TerminalGuiFluentAssertions/ClassDiagram1.cd
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ClassDiagram MajorVersion="1" MinorVersion="1">
|
||||||
|
<Class Name="TerminalGuiFluentTesting.With" Collapsed="true">
|
||||||
|
<Position X="0.5" Y="1.5" Width="1.5" />
|
||||||
|
<TypeIdentifier>
|
||||||
|
<HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAIAAAAAA=</HashCode>
|
||||||
|
<FileName>With.cs</FileName>
|
||||||
|
</TypeIdentifier>
|
||||||
|
</Class>
|
||||||
|
<Class Name="TerminalGuiFluentTesting.FakeInput<T>" Collapsed="true">
|
||||||
|
<Position X="7.5" Y="1.5" Width="1.5" />
|
||||||
|
<TypeIdentifier>
|
||||||
|
<HashCode>AQAAAAAAACAAAQEAAAAgAAAAAAAAAAAAAAAAAAAAAAI=</HashCode>
|
||||||
|
<FileName>FakeInput.cs</FileName>
|
||||||
|
</TypeIdentifier>
|
||||||
|
<Lollipop Position="0.2" />
|
||||||
|
</Class>
|
||||||
|
<Class Name="TerminalGuiFluentTesting.FakeNetInput" Collapsed="true">
|
||||||
|
<Position X="8.75" Y="2.75" Width="1.5" />
|
||||||
|
<TypeIdentifier>
|
||||||
|
<HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
|
||||||
|
<FileName>FakeNetInput.cs</FileName>
|
||||||
|
</TypeIdentifier>
|
||||||
|
<Lollipop Position="0.2" />
|
||||||
|
</Class>
|
||||||
|
<Class Name="TerminalGuiFluentTesting.FakeWindowsInput" Collapsed="true">
|
||||||
|
<Position X="6.5" Y="2.75" Width="1.5" />
|
||||||
|
<TypeIdentifier>
|
||||||
|
<HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
|
||||||
|
<FileName>FakeWindowsInput.cs</FileName>
|
||||||
|
</TypeIdentifier>
|
||||||
|
<Lollipop Position="0.2" />
|
||||||
|
</Class>
|
||||||
|
<Class Name="TerminalGuiFluentTesting.FakeOutput" Collapsed="true" BaseTypeListCollapsed="true">
|
||||||
|
<Position X="6" Y="0.5" Width="1.5" />
|
||||||
|
<TypeIdentifier>
|
||||||
|
<HashCode>AAAAAAAAgCAAgAAAAAAAAAAAAAAAQAAAMAAAAAEAAAA=</HashCode>
|
||||||
|
<FileName>FakeOutput.cs</FileName>
|
||||||
|
</TypeIdentifier>
|
||||||
|
<Lollipop Position="0.2" />
|
||||||
|
</Class>
|
||||||
|
<Class Name="TerminalGuiFluentTesting.GuiTestContext<T>" BaseTypeListCollapsed="true">
|
||||||
|
<Position X="2.75" Y="0.5" Width="2.25" />
|
||||||
|
<Compartments>
|
||||||
|
<Compartment Name="Fields" Collapsed="true" />
|
||||||
|
</Compartments>
|
||||||
|
<TypeIdentifier>
|
||||||
|
<HashCode>ABJAAAIAACBAAQAAgIAAAAAgABIEgAQAIAAIBACAIgA=</HashCode>
|
||||||
|
<FileName>GuiTestContext.cs</FileName>
|
||||||
|
</TypeIdentifier>
|
||||||
|
<ShowAsAssociation>
|
||||||
|
<Field Name="_output" />
|
||||||
|
<Field Name="_winInput" />
|
||||||
|
<Field Name="_netInput" />
|
||||||
|
</ShowAsAssociation>
|
||||||
|
<Lollipop Position="0.2" />
|
||||||
|
</Class>
|
||||||
|
<Font Name="Segoe UI" Size="9" />
|
||||||
|
</ClassDiagram>
|
||||||
34
TerminalGuiFluentAssertions/FakeInput.cs
Normal file
34
TerminalGuiFluentAssertions/FakeInput.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Terminal.Gui;
|
||||||
|
|
||||||
|
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 (new [] { token.WaitHandle, _hardStopToken.WaitHandle, _timeoutCts.Token.WaitHandle });
|
||||||
|
}
|
||||||
|
}
|
||||||
6
TerminalGuiFluentAssertions/FakeNetInput.cs
Normal file
6
TerminalGuiFluentAssertions/FakeNetInput.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
using Terminal.Gui;
|
||||||
|
|
||||||
|
namespace TerminalGuiFluentTesting;
|
||||||
|
|
||||||
|
internal class FakeNetInput (CancellationToken hardStopToken) : FakeInput<ConsoleKeyInfo> (hardStopToken), INetInput
|
||||||
|
{ }
|
||||||
28
TerminalGuiFluentAssertions/FakeOutput.cs
Normal file
28
TerminalGuiFluentAssertions/FakeOutput.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using Terminal.Gui;
|
||||||
|
|
||||||
|
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 GetWindowSize () { return Size; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void SetCursorVisibility (CursorVisibility visibility) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void SetCursorPosition (int col, int row) { }
|
||||||
|
}
|
||||||
6
TerminalGuiFluentAssertions/FakeWindowsInput.cs
Normal file
6
TerminalGuiFluentAssertions/FakeWindowsInput.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
using Terminal.Gui;
|
||||||
|
|
||||||
|
namespace TerminalGuiFluentTesting;
|
||||||
|
|
||||||
|
internal class FakeWindowsInput (CancellationToken hardStopToken) : FakeInput<WindowsConsole.InputRecord> (hardStopToken), IWindowsInput
|
||||||
|
{ }
|
||||||
304
TerminalGuiFluentAssertions/GuiTestContext.cs
Normal file
304
TerminalGuiFluentAssertions/GuiTestContext.cs
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Terminal.Gui;
|
||||||
|
using Terminal.Gui.ConsoleDrivers;
|
||||||
|
|
||||||
|
namespace TerminalGuiFluentTesting;
|
||||||
|
|
||||||
|
public class GuiTestContext<T> : IDisposable where T : Toplevel, new ()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
internal GuiTestContext (int width, int height)
|
||||||
|
{
|
||||||
|
IApplication origApp = ApplicationImpl.Instance;
|
||||||
|
|
||||||
|
_netInput = new (_cts.Token);
|
||||||
|
_winInput = new (_cts.Token);
|
||||||
|
|
||||||
|
_output.Size = new (width, height);
|
||||||
|
|
||||||
|
var v2 = new ApplicationV2 (
|
||||||
|
() => _netInput,
|
||||||
|
() => _output,
|
||||||
|
() => _winInput,
|
||||||
|
() => _output);
|
||||||
|
|
||||||
|
var booting = new SemaphoreSlim (0, 1);
|
||||||
|
|
||||||
|
// Start the application in a background thread
|
||||||
|
_runTask = Task.Run (
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ApplicationImpl.ChangeInstance (v2);
|
||||||
|
|
||||||
|
v2.Init (null, "v2win");
|
||||||
|
|
||||||
|
booting.Release ();
|
||||||
|
|
||||||
|
Application.Run<T> (); // This will block, but it's on a background thread now
|
||||||
|
|
||||||
|
Application.Shutdown ();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{ }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_ex = ex;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ApplicationImpl.ChangeInstance (origApp);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_cts.Token);
|
||||||
|
|
||||||
|
// Wait for booting to complete with a timeout to avoid hangs
|
||||||
|
if (!booting.WaitAsync (TimeSpan.FromSeconds (5)).Result)
|
||||||
|
{
|
||||||
|
throw new TimeoutException ("Application failed to start within the allotted time.");
|
||||||
|
}
|
||||||
|
|
||||||
|
WaitIteration ();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the application and waits for the background thread to exit.
|
||||||
|
/// </summary>
|
||||||
|
public GuiTestContext<T> Stop ()
|
||||||
|
{
|
||||||
|
if (_runTask.IsCompleted)
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Application.Invoke (() => 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 ();
|
||||||
|
|
||||||
|
throw new TimeoutException ("Application failed to stop within the allotted time.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_cts.Cancel ();
|
||||||
|
|
||||||
|
if (_ex != null)
|
||||||
|
{
|
||||||
|
throw _ex; // Propagate any exception that happened in the background task
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup to avoid state bleed between tests
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
_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<T> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GuiTestContext<T> ResizeConsole (int width, int height)
|
||||||
|
{
|
||||||
|
_output.Size = new (width, height);
|
||||||
|
|
||||||
|
return WaitIteration ();
|
||||||
|
}
|
||||||
|
|
||||||
|
public GuiTestContext<T> ScreenShot (string title, TextWriter writer)
|
||||||
|
{
|
||||||
|
writer.WriteLine (title + ":");
|
||||||
|
var text = Application.ToString ();
|
||||||
|
|
||||||
|
writer.WriteLine (text);
|
||||||
|
|
||||||
|
return WaitIteration ();
|
||||||
|
}
|
||||||
|
|
||||||
|
public GuiTestContext<T> WaitIteration (Action? a = null)
|
||||||
|
{
|
||||||
|
a ??= () => { };
|
||||||
|
var ctsLocal = new CancellationTokenSource ();
|
||||||
|
|
||||||
|
Application.Invoke (
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
a ();
|
||||||
|
ctsLocal.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GuiTestContext<T> Assert<T2> (AndConstraint<T2> be) { return this; }
|
||||||
|
|
||||||
|
public GuiTestContext<T> RightClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button3Pressed, screenX, screenY); }
|
||||||
|
|
||||||
|
public GuiTestContext<T> LeftClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY); }
|
||||||
|
|
||||||
|
private GuiTestContext<T> Click (WindowsConsole.ButtonState btn, int screenX, int screenY)
|
||||||
|
{
|
||||||
|
_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)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
WaitIteration ();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GuiTestContext<T> Down ()
|
||||||
|
{
|
||||||
|
_winInput.InputBuffer.Enqueue (
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
EventType = WindowsConsole.EventType.Key,
|
||||||
|
KeyEvent = new()
|
||||||
|
{
|
||||||
|
bKeyDown = true,
|
||||||
|
wRepeatCount = 0,
|
||||||
|
wVirtualKeyCode = ConsoleKeyMapping.VK.DOWN,
|
||||||
|
wVirtualScanCode = 0,
|
||||||
|
UnicodeChar = '\0',
|
||||||
|
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_winInput.InputBuffer.Enqueue (
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
EventType = WindowsConsole.EventType.Key,
|
||||||
|
KeyEvent = new()
|
||||||
|
{
|
||||||
|
bKeyDown = false,
|
||||||
|
wRepeatCount = 0,
|
||||||
|
wVirtualKeyCode = ConsoleKeyMapping.VK.DOWN,
|
||||||
|
wVirtualScanCode = 0,
|
||||||
|
UnicodeChar = '\0',
|
||||||
|
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
WaitIteration ();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GuiTestContext<T> Enter ()
|
||||||
|
{
|
||||||
|
_winInput.InputBuffer.Enqueue (
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
EventType = WindowsConsole.EventType.Key,
|
||||||
|
KeyEvent = new()
|
||||||
|
{
|
||||||
|
bKeyDown = true,
|
||||||
|
wRepeatCount = 0,
|
||||||
|
wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN,
|
||||||
|
wVirtualScanCode = 0,
|
||||||
|
UnicodeChar = '\0',
|
||||||
|
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_winInput.InputBuffer.Enqueue (
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
EventType = WindowsConsole.EventType.Key,
|
||||||
|
KeyEvent = new()
|
||||||
|
{
|
||||||
|
bKeyDown = false,
|
||||||
|
wRepeatCount = 0,
|
||||||
|
wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN,
|
||||||
|
wVirtualScanCode = 0,
|
||||||
|
UnicodeChar = '\0',
|
||||||
|
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
WaitIteration ();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GuiTestContext<T> 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");
|
||||||
|
}
|
||||||
22
TerminalGuiFluentAssertions/With.cs
Normal file
22
TerminalGuiFluentAssertions/With.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using Terminal.Gui;
|
||||||
|
|
||||||
|
namespace TerminalGuiFluentTesting;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entry point to fluent assertions.
|
||||||
|
/// </summary>
|
||||||
|
public static class With
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Entrypoint to fluent assertions
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="width"></param>
|
||||||
|
/// <param name="height"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static GuiTestContext<T> A<T> (int width, int height) where T : Toplevel, new () { return new (width, height); }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The global timeout to allow for any given application to run for before shutting down.
|
||||||
|
/// </summary>
|
||||||
|
public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds (30);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using TerminalGuiFluentAssertions;
|
using TerminalGuiFluentTesting;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace UnitTests.FluentTests;
|
namespace UnitTests.FluentTests;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Terminal.Gui\Terminal.Gui.csproj" />
|
<ProjectReference Include="..\..\Terminal.Gui\Terminal.Gui.csproj" />
|
||||||
<ProjectReference Include="..\..\TerminalGuiFluentAssertions\TerminalGuiFluentAssertions.csproj" />
|
<ProjectReference Include="..\..\TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj" />
|
||||||
<ProjectReference Include="..\..\UICatalog\UICatalog.csproj" />
|
<ProjectReference Include="..\..\UICatalog\UICatalog.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user