mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
This commit is contained in:
@@ -49,11 +49,10 @@ public class NetInputProcessor : InputProcessor<ConsoleKeyInfo>
|
||||
private static string FormatConsoleKeyInfoForTestCase (ConsoleKeyInfo input)
|
||||
{
|
||||
string charLiteral = input.KeyChar == '\0' ? @"'\0'" : $"'{input.KeyChar}'";
|
||||
var expectedLiteral = "new Rune('todo')";
|
||||
|
||||
return $"new ConsoleKeyInfo({charLiteral}, ConsoleKey.{input.Key}, "
|
||||
+ $"{input.Modifiers.HasFlag (ConsoleModifiers.Shift).ToString ().ToLower ()}, "
|
||||
+ $"{input.Modifiers.HasFlag (ConsoleModifiers.Alt).ToString ().ToLower ()}, "
|
||||
+ $"{input.Modifiers.HasFlag (ConsoleModifiers.Control).ToString ().ToLower ()}), {expectedLiteral}";
|
||||
+ $"{input.Modifiers.HasFlag (ConsoleModifiers.Control).ToString ().ToLower ()}),";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
<InternalsVisibleTo Include="StressTests" />
|
||||
<InternalsVisibleTo Include="IntegrationTests" />
|
||||
<InternalsVisibleTo Include="TerminalGuiDesigner" />
|
||||
<InternalsVisibleTo Include="TerminalGuiFluentTesting" />
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
</ItemGroup>
|
||||
<!-- =================================================================== -->
|
||||
|
||||
@@ -84,6 +84,92 @@ public class TabView : View
|
||||
}
|
||||
);
|
||||
|
||||
AddCommand (
|
||||
Command.Up,
|
||||
() =>
|
||||
{
|
||||
if (_style.TabsOnBottom)
|
||||
{
|
||||
if (_tabsBar is { HasFocus: true } && _containerView.CanFocus)
|
||||
{
|
||||
_containerView.SetFocus ();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_containerView is { HasFocus: true })
|
||||
{
|
||||
var mostFocused = _containerView.MostFocused;
|
||||
|
||||
if (mostFocused is { })
|
||||
{
|
||||
for (int? i = mostFocused.SuperView?.SubViews.IndexOf (mostFocused) - 1; i > -1; i--)
|
||||
{
|
||||
var view = mostFocused.SuperView?.SubViews.ElementAt ((int)i);
|
||||
|
||||
if (view is { CanFocus: true, Enabled: true, Visible: true })
|
||||
{
|
||||
// Let toplevel handle it
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SelectedTab?.SetFocus ();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
AddCommand (
|
||||
Command.Down,
|
||||
() =>
|
||||
{
|
||||
if (_style.TabsOnBottom)
|
||||
{
|
||||
if (_containerView is { HasFocus: true })
|
||||
{
|
||||
var mostFocused = _containerView.MostFocused;
|
||||
|
||||
if (mostFocused is { })
|
||||
{
|
||||
for (int? i = mostFocused.SuperView?.SubViews.IndexOf (mostFocused) + 1; i < mostFocused.SuperView?.SubViews.Count; i++)
|
||||
{
|
||||
var view = mostFocused.SuperView?.SubViews.ElementAt ((int)i);
|
||||
|
||||
if (view is { CanFocus: true, Enabled: true, Visible: true })
|
||||
{
|
||||
// Let toplevel handle it
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SelectedTab?.SetFocus ();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_tabsBar is { HasFocus: true } && _containerView.CanFocus)
|
||||
{
|
||||
_containerView.SetFocus ();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
// Default keybindings for this view
|
||||
KeyBindings.Add (Key.CursorLeft, Command.Left);
|
||||
KeyBindings.Add (Key.CursorRight, Command.Right);
|
||||
@@ -91,6 +177,8 @@ public class TabView : View
|
||||
KeyBindings.Add (Key.End, Command.RightEnd);
|
||||
KeyBindings.Add (Key.PageDown, Command.PageDown);
|
||||
KeyBindings.Add (Key.PageUp, Command.PageUp);
|
||||
KeyBindings.Add (Key.CursorUp, Command.Up);
|
||||
KeyBindings.Add (Key.CursorDown, Command.Down);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -155,7 +243,7 @@ public class TabView : View
|
||||
|
||||
private bool TabCanSetFocus ()
|
||||
{
|
||||
return IsInitialized && SelectedTab is { } && (_selectedTabHasFocus || !_containerView.CanFocus);
|
||||
return IsInitialized && SelectedTab is { } && (HasFocus || (bool)_containerView?.HasFocus) && (_selectedTabHasFocus || !_containerView.CanFocus);
|
||||
}
|
||||
|
||||
private void ContainerViewCanFocus (object sender, EventArgs eventArgs)
|
||||
@@ -518,7 +606,7 @@ public class TabView : View
|
||||
{
|
||||
SelectedTab?.SetFocus ();
|
||||
}
|
||||
else
|
||||
else if (HasFocus)
|
||||
{
|
||||
SelectedTab?.View?.SetFocus ();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.2.32427.441
|
||||
@@ -62,6 +63,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StressTests", "Tests\Stress
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable", "Tests\UnitTestsParallelizable\UnitTests.Parallelizable.csproj", "{DE780834-190A-8277-51FD-750CC666E82D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting", "TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj", "{2DBA7BDC-17AE-474B-A507-00807D087607}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -116,6 +119,10 @@ Global
|
||||
{DE780834-190A-8277-51FD-750CC666E82D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DE780834-190A-8277-51FD-750CC666E82D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DE780834-190A-8277-51FD-750CC666E82D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2DBA7BDC-17AE-474B-A507-00807D087607}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2DBA7BDC-17AE-474B-A507-00807D087607}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
89
TerminalGuiFluentTesting/ClassDiagram1.cd
Normal file
89
TerminalGuiFluentTesting/ClassDiagram1.cd
Normal file
@@ -0,0 +1,89 @@
|
||||
<?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" 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.25" 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" Y="2.75" Width="1.75" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
|
||||
<FileName>FakeWindowsInput.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="TerminalGuiFluentTesting.FakeOutput" Collapsed="true" BaseTypeListCollapsed="true">
|
||||
<Position X="5.5" Y="0.5" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAgCAAgAAAAAAAAAAAAAAAQAAAMAAAAAEAAAA=</HashCode>
|
||||
<FileName>FakeOutput.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="TerminalGuiFluentTesting.GuiTestContext" BaseTypeListCollapsed="true">
|
||||
<Position X="2.25" Y="0.5" Width="2.25" />
|
||||
<Compartments>
|
||||
<Compartment Name="Fields" Collapsed="true" />
|
||||
</Compartments>
|
||||
<TypeIdentifier>
|
||||
<HashCode>ABJAAAIAACBACRAAg4IAAAAgAJIEgQQAKACIBACAIgI=</HashCode>
|
||||
<FileName>GuiTestContext.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<ShowAsAssociation>
|
||||
<Field Name="_output" />
|
||||
<Field Name="_winInput" />
|
||||
<Field Name="_netInput" />
|
||||
</ShowAsAssociation>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="TerminalGuiFluentTesting.TextWriterLoggerProvider" Collapsed="true">
|
||||
<Position X="10" Y="2.75" Width="2" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAIAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
|
||||
<FileName>TextWriterLoggerProvider.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="TerminalGuiFluentTesting.TextWriterLogger" Collapsed="true">
|
||||
<Position X="10" Y="1.75" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAAAAAAAAAEAAAAAAAgAAAAAAAAIAAAAAAA=</HashCode>
|
||||
<FileName>TextWriterLogger.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="TerminalGuiFluentTesting.NetSequences">
|
||||
<Position X="11" Y="4.5" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAACACAAAAAAgI=</HashCode>
|
||||
<FileName>NetSequences.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Class>
|
||||
<Enum Name="TerminalGuiFluentTesting.V2TestDriver">
|
||||
<Position X="9.25" Y="4.5" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAACAAA=</HashCode>
|
||||
<FileName>V2TestDriver.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Enum>
|
||||
<Font Name="Segoe UI" Size="9" />
|
||||
</ClassDiagram>
|
||||
34
TerminalGuiFluentTesting/FakeInput.cs
Normal file
34
TerminalGuiFluentTesting/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
TerminalGuiFluentTesting/FakeNetInput.cs
Normal file
6
TerminalGuiFluentTesting/FakeNetInput.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using Terminal.Gui;
|
||||
|
||||
namespace TerminalGuiFluentTesting;
|
||||
|
||||
internal class FakeNetInput (CancellationToken hardStopToken) : FakeInput<ConsoleKeyInfo> (hardStopToken), INetInput
|
||||
{ }
|
||||
28
TerminalGuiFluentTesting/FakeOutput.cs
Normal file
28
TerminalGuiFluentTesting/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
TerminalGuiFluentTesting/FakeWindowsInput.cs
Normal file
6
TerminalGuiFluentTesting/FakeWindowsInput.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using Terminal.Gui;
|
||||
|
||||
namespace TerminalGuiFluentTesting;
|
||||
|
||||
internal class FakeWindowsInput (CancellationToken hardStopToken) : FakeInput<WindowsConsole.InputRecord> (hardStopToken), IWindowsInput
|
||||
{ }
|
||||
551
TerminalGuiFluentTesting/GuiTestContext.cs
Normal file
551
TerminalGuiFluentTesting/GuiTestContext.cs
Normal file
@@ -0,0 +1,551 @@
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Terminal.Gui;
|
||||
using Terminal.Gui.ConsoleDrivers;
|
||||
|
||||
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 StringBuilder _logsSb;
|
||||
private readonly V2TestDriver _driver;
|
||||
|
||||
internal GuiTestContext (Func<Toplevel> topLevelBuilder, int width, int height, V2TestDriver driver)
|
||||
{
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
ILogger logger = LoggerFactory.Create (
|
||||
builder =>
|
||||
builder.SetMinimumLevel (LogLevel.Trace)
|
||||
.AddProvider (new TextWriterLoggerProvider (new StringWriter (_logsSb))))
|
||||
.CreateLogger ("Test Logging");
|
||||
Logging.Logger = logger;
|
||||
|
||||
v2.Init (null, GetDriverName());
|
||||
|
||||
booting.Release ();
|
||||
|
||||
Toplevel t = topLevelBuilder ();
|
||||
|
||||
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);
|
||||
Logging.Logger = origLogger;
|
||||
}
|
||||
},
|
||||
_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 ();
|
||||
}
|
||||
|
||||
private string GetDriverName ()
|
||||
{
|
||||
return _driver switch
|
||||
{
|
||||
V2TestDriver.V2Win => "v2win",
|
||||
V2TestDriver.V2Net => "v2net",
|
||||
_ =>
|
||||
throw new ArgumentOutOfRangeException ()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the application and waits for the background thread to exit.
|
||||
/// </summary>
|
||||
public GuiTestContext 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;
|
||||
}
|
||||
|
||||
/// <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");
|
||||
}
|
||||
|
||||
_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)
|
||||
{
|
||||
_output.Size = new (width, height);
|
||||
|
||||
return WaitIteration ();
|
||||
}
|
||||
|
||||
public GuiTestContext ScreenShot (string title, TextWriter writer)
|
||||
{
|
||||
writer.WriteLine (title + ":");
|
||||
var text = Application.ToString ();
|
||||
|
||||
writer.WriteLine (text);
|
||||
|
||||
return WaitIteration ();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes all Terminal.Gui engine logs collected so far to the <paramref name="writer"/>
|
||||
/// </summary>
|
||||
/// <param name="writer"></param>
|
||||
/// <returns></returns>
|
||||
public GuiTestContext WriteOutLogs (TextWriter writer)
|
||||
{
|
||||
writer.WriteLine (_logsSb.ToString ());
|
||||
|
||||
return WaitIteration ();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits until the end of the current iteration of the main loop. Optionally
|
||||
/// running a given <paramref name="a"/> action on the UI thread at that time.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
/// <returns></returns>
|
||||
public GuiTestContext WaitIteration (Action? a = null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
doAction ();
|
||||
|
||||
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); }
|
||||
|
||||
private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY)
|
||||
{
|
||||
switch (_driver)
|
||||
{
|
||||
case V2TestDriver.V2Win:
|
||||
|
||||
_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)
|
||||
}
|
||||
});
|
||||
break;
|
||||
case V2TestDriver.V2Net:
|
||||
|
||||
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 (var k in NetSequences.Click(netButton,screenX,screenY))
|
||||
{
|
||||
SendNetKey (k);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException ();
|
||||
}
|
||||
|
||||
WaitIteration ();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public GuiTestContext Down ()
|
||||
{
|
||||
switch (_driver)
|
||||
{
|
||||
case V2TestDriver.V2Win:
|
||||
SendWindowsKey (ConsoleKeyMapping.VK.DOWN);
|
||||
WaitIteration ();
|
||||
break;
|
||||
case V2TestDriver.V2Net:
|
||||
foreach (var k in NetSequences.Down)
|
||||
{
|
||||
SendNetKey (k);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException ();
|
||||
}
|
||||
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates the Right cursor key
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public GuiTestContext Right ()
|
||||
{
|
||||
switch (_driver)
|
||||
{
|
||||
case V2TestDriver.V2Win:
|
||||
SendWindowsKey (ConsoleKeyMapping.VK.RIGHT);
|
||||
WaitIteration ();
|
||||
break;
|
||||
case V2TestDriver.V2Net:
|
||||
foreach (var k in NetSequences.Right)
|
||||
{
|
||||
SendNetKey (k);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException ();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates the Left cursor key
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public GuiTestContext Left ()
|
||||
{
|
||||
switch (_driver)
|
||||
{
|
||||
case V2TestDriver.V2Win:
|
||||
SendWindowsKey (ConsoleKeyMapping.VK.LEFT);
|
||||
WaitIteration ();
|
||||
break;
|
||||
case V2TestDriver.V2Net:
|
||||
foreach (var k in NetSequences.Left)
|
||||
{
|
||||
SendNetKey (k);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException ();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates the up cursor key
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public GuiTestContext Up ()
|
||||
{
|
||||
switch (_driver)
|
||||
{
|
||||
case V2TestDriver.V2Win:
|
||||
SendWindowsKey (ConsoleKeyMapping.VK.UP);
|
||||
WaitIteration ();
|
||||
break;
|
||||
case V2TestDriver.V2Net:
|
||||
foreach (var k in NetSequences.Up)
|
||||
{
|
||||
SendNetKey (k);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException ();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates pressing the Return/Enter (newline) key.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public GuiTestContext Enter ()
|
||||
{
|
||||
switch (_driver)
|
||||
{
|
||||
case V2TestDriver.V2Win:
|
||||
SendWindowsKey (
|
||||
new WindowsConsole.KeyEventRecord
|
||||
{
|
||||
UnicodeChar = '\r',
|
||||
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
|
||||
wRepeatCount = 1,
|
||||
wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN,
|
||||
wVirtualScanCode = 28
|
||||
});
|
||||
break;
|
||||
case V2TestDriver.V2Net:
|
||||
SendNetKey (new ('\r', ConsoleKey.Enter, 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="menuItems"/>.
|
||||
/// </summary>
|
||||
/// <param name="ctx"></param>
|
||||
/// <param name="menuItems"></param>
|
||||
/// <returns></returns>
|
||||
public GuiTestContext WithContextMenu (ContextMenu ctx, MenuBarItem menuItems)
|
||||
{
|
||||
LastView.MouseEvent += (s, e) =>
|
||||
{
|
||||
if (e.Flags.HasFlag (MouseFlags.Button3Clicked))
|
||||
{
|
||||
ctx.Show (menuItems);
|
||||
}
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The last view added (e.g. with <see cref="Add"/>) or the root/current top.
|
||||
/// </summary>
|
||||
public View LastView => _lastView ?? Application.Top ?? throw new ("Could not determine which view to add to");
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
_netInput.InputBuffer.Enqueue (consoleKeyInfo);
|
||||
}
|
||||
|
||||
/// <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 ();
|
||||
}
|
||||
}
|
||||
53
TerminalGuiFluentTesting/NetSequences.cs
Normal file
53
TerminalGuiFluentTesting/NetSequences.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace TerminalGuiFluentTesting;
|
||||
class NetSequences
|
||||
{
|
||||
public static ConsoleKeyInfo [] Down = new []
|
||||
{
|
||||
new ConsoleKeyInfo('\x1B', ConsoleKey.Enter, false, false, false),
|
||||
new ConsoleKeyInfo('[', ConsoleKey.None, false, false, false),
|
||||
new ConsoleKeyInfo('B', ConsoleKey.None, false, false, false),
|
||||
};
|
||||
|
||||
public static ConsoleKeyInfo [] Up = new []
|
||||
{
|
||||
new ConsoleKeyInfo('\x1B', ConsoleKey.Enter, false, false, false),
|
||||
new ConsoleKeyInfo('[', ConsoleKey.None, false, false, false),
|
||||
new ConsoleKeyInfo('A', ConsoleKey.None, false, false, false),
|
||||
};
|
||||
|
||||
public static ConsoleKeyInfo [] Left = new []
|
||||
{
|
||||
new ConsoleKeyInfo('\x1B', ConsoleKey.Enter, false, false, false),
|
||||
new ConsoleKeyInfo('[', ConsoleKey.None, false, false, false),
|
||||
new ConsoleKeyInfo('D', ConsoleKey.None, false, false, false),
|
||||
};
|
||||
|
||||
public static ConsoleKeyInfo [] Right = new []
|
||||
{
|
||||
new ConsoleKeyInfo('\x1B', ConsoleKey.Enter, false, false, false),
|
||||
new ConsoleKeyInfo('[', ConsoleKey.None, false, false, false),
|
||||
new ConsoleKeyInfo('C', ConsoleKey.None, false, false, false),
|
||||
};
|
||||
|
||||
public static IEnumerable<ConsoleKeyInfo> Click (int button, int screenX, int screenY)
|
||||
{
|
||||
// Adjust for 1-based coordinates
|
||||
int adjustedX = screenX + 1;
|
||||
int adjustedY = screenY + 1;
|
||||
|
||||
// Mouse press sequence
|
||||
var sequence = $"\x1B[<{button};{adjustedX};{adjustedY}M";
|
||||
foreach (char c in sequence)
|
||||
{
|
||||
yield return new ConsoleKeyInfo (c, ConsoleKey.None, false, false, false);
|
||||
}
|
||||
|
||||
// Mouse release sequence
|
||||
sequence = $"\x1B[<{button};{adjustedX};{adjustedY}m";
|
||||
foreach (char c in sequence)
|
||||
{
|
||||
yield return new ConsoleKeyInfo (c, ConsoleKey.None, false, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
14
TerminalGuiFluentTesting/TerminalGuiFluentTesting.csproj
Normal file
14
TerminalGuiFluentTesting/TerminalGuiFluentTesting.csproj
Normal file
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<DocumentationFile>bin\$(Configuration)\$(AssemblyName).xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
21
TerminalGuiFluentTesting/TextWriterLogger.cs
Normal file
21
TerminalGuiFluentTesting/TextWriterLogger.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace TerminalGuiFluentTesting;
|
||||
|
||||
internal class TextWriterLogger (TextWriter writer) : ILogger
|
||||
{
|
||||
public IDisposable? BeginScope<TState> (TState state) { return null; }
|
||||
|
||||
public bool IsEnabled (LogLevel logLevel) { return true; }
|
||||
|
||||
public void Log<TState> (
|
||||
LogLevel logLevel,
|
||||
EventId eventId,
|
||||
TState state,
|
||||
Exception? ex,
|
||||
Func<TState, Exception?, string> formatter
|
||||
)
|
||||
{
|
||||
writer.WriteLine (formatter (state, ex));
|
||||
}
|
||||
}
|
||||
10
TerminalGuiFluentTesting/TextWriterLoggerProvider.cs
Normal file
10
TerminalGuiFluentTesting/TextWriterLoggerProvider.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace TerminalGuiFluentTesting;
|
||||
|
||||
internal class TextWriterLoggerProvider (TextWriter writer) : ILoggerProvider
|
||||
{
|
||||
public ILogger CreateLogger (string category) { return new TextWriterLogger (writer); }
|
||||
|
||||
public void Dispose () { writer.Dispose (); }
|
||||
}
|
||||
23
TerminalGuiFluentTesting/V2TestDriver.cs
Normal file
23
TerminalGuiFluentTesting/V2TestDriver.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TerminalGuiFluentTesting;
|
||||
|
||||
/// <summary>
|
||||
/// Which v2 driver simulation should be used
|
||||
/// </summary>
|
||||
public enum V2TestDriver
|
||||
{
|
||||
/// <summary>
|
||||
/// The v2 windows driver with simulation I/O but core driver classes
|
||||
/// </summary>
|
||||
V2Win,
|
||||
|
||||
/// <summary>
|
||||
/// The v2 net driver with simulation I/O but core driver classes
|
||||
/// </summary>
|
||||
V2Net
|
||||
}
|
||||
26
TerminalGuiFluentTesting/With.cs
Normal file
26
TerminalGuiFluentTesting/With.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
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>
|
||||
/// <param name="v2TestDriver">Which v2 v2TestDriver to use for the test</param>
|
||||
/// <returns></returns>
|
||||
public static GuiTestContext A<T> (int width, int height, V2TestDriver v2TestDriver) where T : Toplevel, new ()
|
||||
{
|
||||
return new (() => new T (), width, height,v2TestDriver);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
138
Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs
Normal file
138
Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using System.Text;
|
||||
using Terminal.Gui;
|
||||
using TerminalGuiFluentTesting;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace IntegrationTests.FluentTests;
|
||||
|
||||
public class BasicFluentAssertionTests
|
||||
{
|
||||
private readonly TextWriter _out;
|
||||
|
||||
public class TestOutputWriter : TextWriter
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public TestOutputWriter (ITestOutputHelper output) { _output = output; }
|
||||
|
||||
public override void WriteLine (string? value) { _output.WriteLine (value ?? string.Empty); }
|
||||
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
}
|
||||
|
||||
public BasicFluentAssertionTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); }
|
||||
|
||||
[Theory]
|
||||
[ClassData (typeof (V2TestDrivers))]
|
||||
public void GuiTestContext_StartsAndStopsWithoutError (V2TestDriver d)
|
||||
{
|
||||
using GuiTestContext context = With.A<Window> (40, 10,d);
|
||||
|
||||
// No actual assertions are needed — if no exceptions are thrown, it's working
|
||||
context.Stop ();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[ClassData (typeof (V2TestDrivers))]
|
||||
public void GuiTestContext_ForgotToStop (V2TestDriver d)
|
||||
{
|
||||
using GuiTestContext context = With.A<Window> (40, 10, d);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[ClassData (typeof (V2TestDrivers))]
|
||||
public void TestWindowsResize (V2TestDriver d)
|
||||
{
|
||||
var lbl = new Label
|
||||
{
|
||||
Width = Dim.Fill ()
|
||||
};
|
||||
|
||||
using GuiTestContext c = With.A<Window> (40, 10, d)
|
||||
.Add (lbl)
|
||||
.Then (() => Assert.Equal (38, lbl.Frame.Width)) // Window has 2 border
|
||||
.ResizeConsole (20, 20)
|
||||
.Then (() => Assert.Equal (18, lbl.Frame.Width))
|
||||
.WriteOutLogs (_out)
|
||||
.Stop ();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[ClassData (typeof (V2TestDrivers))]
|
||||
public void ContextMenu_CrashesOnRight (V2TestDriver d)
|
||||
{
|
||||
var clicked = false;
|
||||
|
||||
var ctx = new ContextMenu ();
|
||||
|
||||
var menuItems = new MenuBarItem (
|
||||
[
|
||||
new ("_New File", string.Empty, () => { clicked = true; })
|
||||
]
|
||||
);
|
||||
|
||||
using GuiTestContext c = With.A<Window> (40, 10, d)
|
||||
.WithContextMenu (ctx, menuItems)
|
||||
.ScreenShot ("Before open menu", _out)
|
||||
|
||||
// Click in main area inside border
|
||||
.RightClick (1, 1)
|
||||
.ScreenShot ("After open menu", _out)
|
||||
.LeftClick (3, 3)
|
||||
.Stop ()
|
||||
.WriteOutLogs (_out);
|
||||
Assert.True (clicked);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[ClassData (typeof (V2TestDrivers))]
|
||||
public void ContextMenu_OpenSubmenu (V2TestDriver d)
|
||||
{
|
||||
var clicked = false;
|
||||
|
||||
var ctx = new ContextMenu ();
|
||||
|
||||
|
||||
|
||||
var menuItems = new MenuBarItem (
|
||||
[
|
||||
new MenuItem ("One", "", null),
|
||||
new MenuItem ("Two", "", null),
|
||||
new MenuItem ("Three", "", null),
|
||||
new MenuBarItem (
|
||||
"Four",
|
||||
[
|
||||
new MenuItem ("SubMenu1", "", null),
|
||||
new MenuItem ("SubMenu2", "", ()=>clicked=true),
|
||||
new MenuItem ("SubMenu3", "", null),
|
||||
new MenuItem ("SubMenu4", "", null),
|
||||
new MenuItem ("SubMenu5", "", null),
|
||||
new MenuItem ("SubMenu6", "", null),
|
||||
new MenuItem ("SubMenu7", "", null)
|
||||
]
|
||||
),
|
||||
new MenuItem ("Five", "", null),
|
||||
new MenuItem ("Six", "", null)
|
||||
]
|
||||
);
|
||||
|
||||
using GuiTestContext c = With.A<Window> (40, 10,d)
|
||||
.WithContextMenu (ctx, menuItems)
|
||||
.ScreenShot ("Before open menu", _out)
|
||||
|
||||
// Click in main area inside border
|
||||
.RightClick (1, 1)
|
||||
.ScreenShot ("After open menu", _out)
|
||||
.Down ()
|
||||
.Down ()
|
||||
.Down ()
|
||||
.Right()
|
||||
.ScreenShot ("After open submenu", _out)
|
||||
.Down ()
|
||||
.Enter ()
|
||||
.ScreenShot ("Menu should be closed after selecting", _out)
|
||||
.Stop ()
|
||||
.WriteOutLogs (_out);
|
||||
Assert.True (clicked);
|
||||
}
|
||||
}
|
||||
15
Tests/IntegrationTests/FluentTests/V2TestDrivers.cs
Normal file
15
Tests/IntegrationTests/FluentTests/V2TestDrivers.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Collections;
|
||||
using TerminalGuiFluentTesting;
|
||||
|
||||
namespace IntegrationTests.FluentTests;
|
||||
|
||||
public class V2TestDrivers : IEnumerable<object []>
|
||||
{
|
||||
public IEnumerator<object []> GetEnumerator ()
|
||||
{
|
||||
yield return new object [] { V2TestDriver.V2Win };
|
||||
yield return new object [] { V2TestDriver.V2Net };
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Terminal.Gui\Terminal.Gui.csproj" />
|
||||
<ProjectReference Include="..\..\TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj" />
|
||||
<ProjectReference Include="..\..\UICatalog\UICatalog.csproj" />
|
||||
<ProjectReference Include="..\UnitTests\UnitTests.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -6,7 +6,7 @@ using Moq;
|
||||
namespace UnitTests.ConsoleDrivers.V2;
|
||||
public class ApplicationV2Tests
|
||||
{
|
||||
|
||||
|
||||
private ApplicationV2 NewApplicationV2 ()
|
||||
{
|
||||
var netInput = new Mock<INetInput> ();
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace UnitTests.ConsoleDrivers.V2;
|
||||
public class MainLoopCoordinatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void TestMainLoopCoordinator_InputCrashes_ExceptionSurfacesMainThread ()
|
||||
public async Task TestMainLoopCoordinator_InputCrashes_ExceptionSurfacesMainThread ()
|
||||
{
|
||||
|
||||
var mockLogger = new Mock<ILogger> ();
|
||||
@@ -26,7 +26,7 @@ public class MainLoopCoordinatorTests
|
||||
|
||||
// StartAsync boots the main loop and the input thread. But if the input class bombs
|
||||
// on startup it is important that the exception surface at the call site and not lost
|
||||
var ex = Assert.ThrowsAsync<AggregateException>(c.StartAsync).Result;
|
||||
var ex = await Assert.ThrowsAsync<AggregateException>(c.StartAsync);
|
||||
Assert.Equal ("Crash on boot", ex.InnerExceptions [0].Message);
|
||||
|
||||
|
||||
|
||||
@@ -2135,4 +2135,84 @@ public class ContextMenuTests (ITestOutputHelper output)
|
||||
|
||||
top.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[AutoInitShutdown]
|
||||
public void Menu_Opened_In_SuperView_With_TabView_Has_Precedence_On_Key_Press ()
|
||||
{
|
||||
var win = new Window
|
||||
{
|
||||
Title = "My Window",
|
||||
X = 0,
|
||||
Y = 0,
|
||||
Width = Dim.Fill (),
|
||||
Height = Dim.Fill ()
|
||||
};
|
||||
|
||||
// Tab View
|
||||
var tabView = new TabView
|
||||
{
|
||||
X = 1,
|
||||
Y = 1,
|
||||
Width = Dim.Fill () - 2,
|
||||
Height = Dim.Fill () - 2
|
||||
};
|
||||
tabView.AddTab (new () { DisplayText = "Tab 1" }, true);
|
||||
tabView.AddTab (new () { DisplayText = "Tab 2" }, false);
|
||||
win.Add (tabView);
|
||||
|
||||
// Context Menu
|
||||
var menuItems = new MenuBarItem (
|
||||
[
|
||||
new ("Item 1", "First item", () => MessageBox.Query ("Action", "Item 1 Clicked", "OK")),
|
||||
new MenuBarItem (
|
||||
"Submenu",
|
||||
new List<MenuItem []>
|
||||
{
|
||||
new []
|
||||
{
|
||||
new MenuItem (
|
||||
"Sub Item 1",
|
||||
"Submenu item",
|
||||
() => { MessageBox.Query ("Action", "Sub Item 1 Clicked", "OK"); })
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
var cm = new ContextMenu ();
|
||||
|
||||
win.MouseClick += (s, e) =>
|
||||
{
|
||||
if (e.Flags.HasFlag (MouseFlags.Button3Clicked)) // Right-click
|
||||
{
|
||||
cm.Position = e.Position;
|
||||
cm.Show (menuItems);
|
||||
}
|
||||
};
|
||||
Application.Begin (win);
|
||||
|
||||
cm.Show (menuItems);
|
||||
Assert.True (cm.MenuBar!.IsMenuOpen);
|
||||
|
||||
Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown));
|
||||
Assert.True (cm.MenuBar!.IsMenuOpen);
|
||||
|
||||
Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp));
|
||||
Assert.True (cm.MenuBar!.IsMenuOpen);
|
||||
|
||||
Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown));
|
||||
Assert.True (cm.MenuBar!.IsMenuOpen);
|
||||
|
||||
Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight));
|
||||
Assert.True (cm.MenuBar!.IsMenuOpen);
|
||||
|
||||
Assert.True (Application.RaiseKeyDownEvent (Key.CursorLeft));
|
||||
Assert.True (cm.MenuBar!.IsMenuOpen);
|
||||
|
||||
Assert.True (Application.RaiseKeyDownEvent (Key.CursorLeft));
|
||||
Assert.False (cm.MenuBar!.IsMenuOpen);
|
||||
Assert.True (tabView.HasFocus);
|
||||
|
||||
win.Dispose ();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,9 +434,8 @@ public class TabViewTests (ITestOutputHelper output)
|
||||
Assert.Equal (btnSubView, top.MostFocused);
|
||||
|
||||
Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp));
|
||||
// TabRow now has TabGroup which only F6 is allowed
|
||||
Assert.NotEqual (tab2, top.MostFocused);
|
||||
Assert.Equal (btn, top.MostFocused);
|
||||
Assert.Equal (tab2, top.MostFocused);
|
||||
Assert.NotEqual (btn, top.MostFocused);
|
||||
|
||||
Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp));
|
||||
Assert.Equal (btnSubView, top.MostFocused);
|
||||
@@ -459,7 +458,8 @@ public class TabViewTests (ITestOutputHelper output)
|
||||
// Press the cursor up key to focus the selected tab which it's the only way to do that
|
||||
Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp));
|
||||
Assert.Equal (tab2, tv.SelectedTab);
|
||||
Assert.Equal (btn, top.Focused);
|
||||
Assert.False (tab2.View.HasFocus);
|
||||
Assert.Equal (tv, top.Focused);
|
||||
|
||||
Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp));
|
||||
Assert.Equal (tv, top.Focused);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"UICatalog --driver v2net": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "--driver v2net"
|
||||
"commandLineArgs": "--driver v2net -dl Trace"
|
||||
},
|
||||
"WSL: UICatalog": {
|
||||
"commandName": "Executable",
|
||||
|
||||
Reference in New Issue
Block a user