Fixes #4456 - Clear MouseGrabView in App.End (#4460)

* Fixed MouseGrabView bug.

Added extensive test coverage for `Keyboard`, `Mouse`, `Timeout`, and `Popover` functionalities, including edge cases and concurrent access. Introduced parameterized and data-driven tests to reduce redundancy and improve clarity.

Refactored codebase for modularity and maintainability,
introducing new namespaces and reorganizing classes. Enhanced `MouseImpl`, `KeyboardImpl`, and `Runnable` implementations with improved event handling, thread safety, and support for the Terminal.Gui Cancellable Work Pattern (CWP).

Removed deprecated code and legacy tests, such as `LogarithmicTimeout` and `SmoothAcceleratingTimeout`. Fixed bugs related to mouse grabbing during drag operations and unbalanced `ApplicationImpl.Begin/End` calls. Improved documentation and code readability with modern C# features.

* Code cleanup.

* Update Tests/UnitTestsParallelizable/Application/Runnable/RunnableIntegrationTests.cs

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

* Improve null handling and simplify test setup

In `MouseImpl.cs`, added an early `return` after the `UngrabMouse()`
call within the `if (view is null)` block to prevent further execution
when `view` is `null`, improving null reference handling.

In `RunnableIntegrationTests.cs`, removed the initialization of the
`IApplication` object (`app`) from the `MultipleRunnables_IndependentResults`
test method, simplifying the test setup and focusing on runnable behavior.

* Code cleanup

* API doc link cleanup

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Tig
2025-12-07 13:01:19 -07:00
committed by GitHub
parent 0270183686
commit 5da7e59aa2
32 changed files with 791 additions and 768 deletions

View File

@@ -1,6 +1,6 @@
using Xunit.Abstractions;
namespace ApplicationTests;
namespace ApplicationTests.Navigation;
public class ApplicationNavigationTests (ITestOutputHelper output)
{

View File

@@ -5,6 +5,66 @@ namespace ApplicationTests;
public class ApplicationImplTests
{
[Fact]
public void Internal_Properties_Correct ()
{
IApplication app = Application.Create ();
app.Init ("fake");
Assert.True (app.Initialized);
Assert.Null (app.TopRunnableView);
SessionToken? rs = app.Begin (new Runnable<bool> ());
Assert.Equal (app.TopRunnable, rs!.Runnable);
Assert.Null (app.Mouse.MouseGrabView); // public
app.Dispose ();
}
#region DisposeTests
[Fact]
public async Task Dispose_Allows_Async ()
{
var isCompletedSuccessfully = false;
async Task TaskWithAsyncContinuation ()
{
await Task.Yield ();
await Task.Yield ();
isCompletedSuccessfully = true;
}
IApplication app = Application.Create ();
app.Dispose ();
Assert.False (isCompletedSuccessfully);
await TaskWithAsyncContinuation ();
Thread.Sleep (100);
Assert.True (isCompletedSuccessfully);
}
[Fact]
public void Dispose_Resets_SyncContext ()
{
IApplication app = Application.Create ();
app.Dispose ();
Assert.Null (SynchronizationContext.Current);
}
[Fact]
public void Dispose_Alone_Does_Nothing ()
{
IApplication app = Application.Create ();
app.Dispose ();
}
#endregion
/// <summary>
/// Crates a new ApplicationImpl instance for testing. The input, output, and size monitor components are mocked.
/// </summary>
@@ -44,21 +104,6 @@ public class ApplicationImplTests
.Verifiable (Times.Once);
}
[Fact]
public void Init_CreatesKeybindings ()
{
IApplication app = NewMockedApplicationImpl ();
app.Keyboard.KeyBindings.Clear ();
Assert.Empty (app.Keyboard.KeyBindings.GetBindings ());
app.Init ("fake");
Assert.NotEmpty (app.Keyboard.KeyBindings.GetBindings ());
app.Dispose ();
}
[Fact]
public void NoInitThrowOnRun ()
@@ -480,81 +525,4 @@ public class ApplicationImplTests
Assert.Null (v2.TopRunnableView);
Assert.Empty (v2.SessionStack!);
}
[Fact]
public void Init_Begin_End_Cleans_Up ()
{
IApplication? app = Application.Create ();
SessionToken? newSessionToken = null;
EventHandler<SessionTokenEventArgs> newSessionTokenFn = (s, e) =>
{
Assert.NotNull (e.State);
newSessionToken = e.State;
};
app.SessionBegun += newSessionTokenFn;
Runnable<bool> runnable = new ();
SessionToken sessionToken = app.Begin (runnable)!;
Assert.NotNull (sessionToken);
Assert.NotNull (newSessionToken);
Assert.Equal (sessionToken, newSessionToken);
// Assert.Equal (runnable, Application.TopRunnable);
app.SessionBegun -= newSessionTokenFn;
app.End (newSessionToken);
Assert.Null (app.TopRunnable);
Assert.Null (app.Driver);
runnable.Dispose ();
}
[Fact]
public void Run_RequestStop_Stops ()
{
IApplication? app = Application.Create ();
app.Init ("fake");
var top = new Runnable ();
SessionToken? sessionToken = app.Begin (top);
Assert.NotNull (sessionToken);
app.Iteration += OnApplicationOnIteration;
app.Run (top);
app.Iteration -= OnApplicationOnIteration;
top.Dispose ();
return;
void OnApplicationOnIteration (object? s, EventArgs<IApplication?> a) { app.RequestStop (); }
}
[Fact]
public void Run_T_Init_Driver_Cleared_with_Runnable_Throws ()
{
IApplication? app = Application.Create ();
app.Init ("fake");
app.Driver = null;
app.StopAfterFirstIteration = true;
// Init has been called, but Driver has been set to null. Bad.
Assert.Throws<InvalidOperationException> (() => app.Run<Runnable> ());
}
[Fact]
public void Init_Unbalanced_Throws ()
{
IApplication? app = Application.Create ();
app.Init ("fake");
Assert.Throws<InvalidOperationException> (() =>
app.Init ("fake")
);
}
}

View File

@@ -1,510 +0,0 @@
#nullable enable
using Xunit.Abstractions;
namespace ApplicationTests;
/// <summary>
/// Parallelizable tests for IApplication that don't require the main event loop.
/// Tests using the modern non-static IApplication API.
/// </summary>
public class ApplicationTests (ITestOutputHelper output)
{
private readonly ITestOutputHelper _output = output;
[Fact]
public void Begin_Null_Runnable_Throws ()
{
IApplication app = Application.Create ();
app.Init ("fake");
// Test null Runnable
Assert.Throws<ArgumentNullException> (() => app.Begin (null!));
app.Dispose ();
}
[Fact]
public void Begin_Sets_Application_Top_To_Console_Size ()
{
IApplication app = Application.Create ();
app.Init ("fake");
Assert.Null (app.TopRunnableView);
app.Driver!.SetScreenSize (80, 25);
Runnable top = new ();
SessionToken? token = app.Begin (top);
Assert.Equal (new (0, 0, 80, 25), app.TopRunnableView!.Frame);
app.Driver!.SetScreenSize (5, 5);
app.LayoutAndDraw ();
Assert.Equal (new (0, 0, 5, 5), app.TopRunnableView!.Frame);
if (token is { })
{
app.End (token);
}
top.Dispose ();
app.Dispose ();
}
[Fact]
public void Init_Null_Driver_Should_Pick_A_Driver ()
{
IApplication app = Application.Create ();
app.Init ();
Assert.NotNull (app.Driver);
app.Dispose ();
}
[Fact]
public void Init_Dispose_Cleans_Up ()
{
IApplication app = Application.Create ();
app.Init ("fake");
app.Dispose ();
#if DEBUG_IDISPOSABLE
// Validate there are no outstanding Responder-based instances
// after cleanup
// Note: We can't check View.Instances in parallel tests as it's a static field
// that would be shared across parallel test runs
#endif
}
[Fact]
public void Init_Dispose_Fire_InitializedChanged ()
{
var initialized = false;
var Dispose = false;
IApplication app = Application.Create ();
app.InitializedChanged += OnApplicationOnInitializedChanged;
app.Init (driverName: "fake");
Assert.True (initialized);
Assert.False (Dispose);
app.Dispose ();
Assert.True (initialized);
Assert.True (Dispose);
app.InitializedChanged -= OnApplicationOnInitializedChanged;
return;
void OnApplicationOnInitializedChanged (object? s, EventArgs<bool> a)
{
if (a.Value)
{
initialized = true;
}
else
{
Dispose = true;
}
}
}
[Fact]
public void Init_KeyBindings_Are_Not_Reset ()
{
IApplication app = Application.Create ();
// Set via Keyboard property (modern API)
app.Keyboard.QuitKey = Key.Q;
Assert.Equal (Key.Q, app.Keyboard.QuitKey);
app.Init ("fake");
Assert.Equal (Key.Q, app.Keyboard.QuitKey);
app.Dispose ();
}
[Fact]
public void Init_NoParam_ForceDriver_Works ()
{
using IApplication app = Application.Create ();
app.ForceDriver = "fake";
// Note: Init() without params picks up driver configuration
app.Init ();
Assert.Equal ("fake", app.Driver!.GetName ());
}
[Fact]
public void Init_Dispose_Resets_Instance_Properties ()
{
IApplication app = Application.Create ();
// Init the app
app.Init (driverName: "fake");
// Verify initialized
Assert.True (app.Initialized);
Assert.NotNull (app.Driver);
// Dispose cleans up
app.Dispose ();
// Check reset state on the instance
CheckReset (app);
// Create a new instance and set values
app = Application.Create ();
app.Init ("fake");
app.StopAfterFirstIteration = true;
app.Keyboard.PrevTabGroupKey = Key.A;
app.Keyboard.NextTabGroupKey = Key.B;
app.Keyboard.QuitKey = Key.C;
app.Keyboard.KeyBindings.Add (Key.D, Command.Cancel);
app.Mouse.CachedViewsUnderMouse.Clear ();
app.Mouse.LastMousePosition = new Point (1, 1);
// Dispose and check reset
app.Dispose ();
CheckReset (app);
return;
void CheckReset (IApplication application)
{
// Check that all fields and properties are reset on the instance
// Public Properties
Assert.Null (application.TopRunnableView);
Assert.Null (application.Mouse.MouseGrabView);
Assert.Null (application.Driver);
Assert.False (application.StopAfterFirstIteration);
// Internal properties
Assert.False (application.Initialized);
Assert.Null (application.MainThreadId);
Assert.Empty (application.Mouse.CachedViewsUnderMouse);
}
}
[Fact]
public void Internal_Properties_Correct ()
{
IApplication app = Application.Create ();
app.Init ("fake");
Assert.True (app.Initialized);
Assert.Null (app.TopRunnableView);
SessionToken? rs = app.Begin (new Runnable<bool> ());
Assert.Equal (app.TopRunnable, rs!.Runnable);
Assert.Null (app.Mouse.MouseGrabView); // public
app.Dispose ();
}
[Fact]
public void Invoke_Adds_Idle ()
{
IApplication app = Application.Create ();
app.Init ("fake");
Runnable top = new ();
SessionToken? rs = app.Begin (top);
var actionCalled = 0;
app.Invoke ((_) => { actionCalled++; });
app.TimedEvents!.RunTimers ();
Assert.Equal (1, actionCalled);
top.Dispose ();
app.Dispose ();
}
[Fact]
public void Run_Iteration_Fires ()
{
var iteration = 0;
IApplication app = Application.Create ();
app.Init ("fake");
app.Iteration += Application_Iteration;
app.Run<Runnable> ();
app.Iteration -= Application_Iteration;
Assert.Equal (1, iteration);
app.Dispose ();
return;
void Application_Iteration (object? sender, EventArgs<IApplication?> e)
{
iteration++;
app.RequestStop ();
}
}
[Fact]
public void Screen_Size_Changes ()
{
IApplication app = Application.Create ();
app.Init ("fake");
IDriver? driver = app.Driver;
app.Driver!.SetScreenSize (80, 25);
Assert.Equal (new (0, 0, 80, 25), driver!.Screen);
Assert.Equal (new (0, 0, 80, 25), app.Screen);
// TODO: Should not be possible to manually change these at whim!
driver.Cols = 100;
driver.Rows = 30;
app.Driver!.SetScreenSize (100, 30);
Assert.Equal (new (0, 0, 100, 30), driver.Screen);
app.Screen = new (0, 0, driver.Cols, driver.Rows);
Assert.Equal (new (0, 0, 100, 30), driver.Screen);
app.Dispose ();
}
[Fact]
public void Dispose_Alone_Does_Nothing ()
{
IApplication app = Application.Create ();
app.Dispose ();
}
#region RunTests
[Fact]
public void Run_T_After_InitWithDriver_with_Runnable_and_Driver_Does_Not_Throw ()
{
IApplication app = Application.Create ();
app.StopAfterFirstIteration = true;
// Run<Runnable<bool>> when already initialized or not with a Driver will not throw (because Window is derived from Runnable)
// Using another type not derived from Runnable will throws at compile time
app.Run<Window> (null, "fake");
// Run<Runnable<bool>> when already initialized or not with a Driver will not throw (because Dialog is derived from Runnable)
app.Run<Dialog> (null, "fake");
app.Dispose ();
}
[Fact]
public void Run_T_After_Init_Does_Not_Disposes_Application_Top ()
{
IApplication app = Application.Create ();
app.Init ("fake");
// Init doesn't create a Runnable and assigned it to app.TopRunnable
// but Begin does
var initTop = new Runnable ();
app.Iteration += OnApplicationOnIteration;
app.Run<Runnable> ();
app.Iteration -= OnApplicationOnIteration;
#if DEBUG_IDISPOSABLE
Assert.False (initTop.WasDisposed);
initTop.Dispose ();
Assert.True (initTop.WasDisposed);
#endif
initTop.Dispose ();
app.Dispose ();
return;
void OnApplicationOnIteration (object? s, EventArgs<IApplication?> a)
{
Assert.NotEqual (initTop, app.TopRunnableView);
#if DEBUG_IDISPOSABLE
Assert.False (initTop.WasDisposed);
#endif
app.RequestStop ();
}
}
[Fact]
public void Run_T_After_InitWithDriver_with_TestRunnable_DoesNotThrow ()
{
IApplication app = Application.Create ();
app.Init ("fake");
app.StopAfterFirstIteration = true;
// Init has been called and we're passing no driver to Run<TestRunnable>. This is ok.
app.Run<Window> ();
app.Dispose ();
}
[Fact]
public void Run_T_After_InitNullDriver_with_TestRunnable_DoesNotThrow ()
{
IApplication app = Application.Create ();
app.Init ("fake");
app.StopAfterFirstIteration = true;
// Init has been called, selecting FakeDriver; we're passing no driver to Run<TestRunnable>. Should be fine.
app.Run<Window> ();
app.Dispose ();
}
[Fact]
public void Run_T_NoInit_DoesNotThrow ()
{
IApplication app = Application.Create ();
app.StopAfterFirstIteration = true;
app.Run<Window> ();
app.Dispose ();
}
[Fact]
public void Run_T_NoInit_WithDriver_DoesNotThrow ()
{
IApplication app = Application.Create ();
app.StopAfterFirstIteration = true;
// Init has NOT been called and we're passing a valid driver to Run<TestRunnable>. This is ok.
app.Run<Runnable> (null, "fake");
app.Dispose ();
}
[Fact]
public void Run_Sets_Running_True ()
{
IApplication app = Application.Create ();
app.Init ("fake");
var top = new Runnable ();
SessionToken? rs = app.Begin (top);
Assert.NotNull (rs);
app.Iteration += OnApplicationOnIteration;
app.Run (top);
app.Iteration -= OnApplicationOnIteration;
top.Dispose ();
app.Dispose ();
return;
void OnApplicationOnIteration (object? s, EventArgs<IApplication?> a)
{
Assert.True (top.IsRunning);
top.RequestStop ();
}
}
[Fact]
public void Run_A_Modal_Runnable_Refresh_Background_On_Moving ()
{
IApplication app = Application.Create ();
app.Init ("fake");
// Don't use Dialog here as it has more layout logic. Use Window instead.
var w = new Window
{
Width = 5, Height = 5,
Arrangement = ViewArrangement.Movable
};
app.Driver!.SetScreenSize (10, 10);
SessionToken? rs = app.Begin (w);
// Don't use visuals to test as style of border can change over time.
Assert.Equal (new (0, 0), w.Frame.Location);
app.Mouse.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed });
Assert.Equal (w.Border, app.Mouse.MouseGrabView);
Assert.Equal (new (0, 0), w.Frame.Location);
// Move down and to the right.
app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (1, 1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition });
Assert.Equal (new (1, 1), w.Frame.Location);
app.End (rs!);
w.Dispose ();
app.Dispose ();
}
[Fact]
public void Run_T_Creates_Top_Without_Init ()
{
IApplication app = Application.Create ();
app.StopAfterFirstIteration = true;
app.SessionEnded += OnApplicationOnSessionEnded;
app.Run<Window> (null, "fake");
Assert.Null (app.TopRunnableView);
app.Dispose ();
Assert.Null (app.TopRunnableView);
return;
void OnApplicationOnSessionEnded (object? sender, SessionTokenEventArgs e)
{
app.SessionEnded -= OnApplicationOnSessionEnded;
e.State.Result = (e.State.Runnable as IRunnable<object?>)?.Result;
}
}
#endregion
#region DisposeTests
[Fact]
public async Task Dispose_Allows_Async ()
{
var isCompletedSuccessfully = false;
async Task TaskWithAsyncContinuation ()
{
await Task.Yield ();
await Task.Yield ();
isCompletedSuccessfully = true;
}
IApplication app = Application.Create ();
app.Dispose ();
Assert.False (isCompletedSuccessfully);
await TaskWithAsyncContinuation ();
Thread.Sleep (100);
Assert.True (isCompletedSuccessfully);
}
[Fact]
public void Dispose_Resets_SyncContext ()
{
IApplication app = Application.Create ();
app.Dispose ();
Assert.Null (SynchronizationContext.Current);
}
#endregion
}

View File

@@ -1,6 +1,6 @@
using Xunit.Abstractions;
namespace ApplicationTests;
namespace ApplicationTests.BeginEnd;
/// <summary>
/// Comprehensive tests for ApplicationImpl.Begin/End logic that manages Current and SessionStack.
@@ -11,6 +11,74 @@ public class ApplicationImplBeginEndTests (ITestOutputHelper output)
{
private readonly ITestOutputHelper _output = output;
[Fact]
public void Init_Begin_End_Cleans_Up ()
{
IApplication? app = Application.Create ();
SessionToken? newSessionToken = null;
EventHandler<SessionTokenEventArgs> newSessionTokenFn = (s, e) =>
{
Assert.NotNull (e.State);
newSessionToken = e.State;
};
app.SessionBegun += newSessionTokenFn;
Runnable<bool> runnable = new ();
SessionToken sessionToken = app.Begin (runnable)!;
Assert.NotNull (sessionToken);
Assert.NotNull (newSessionToken);
Assert.Equal (sessionToken, newSessionToken);
// Assert.Equal (runnable, Application.TopRunnable);
app.SessionBegun -= newSessionTokenFn;
app.End (newSessionToken);
Assert.Null (app.TopRunnable);
Assert.Null (app.Driver);
runnable.Dispose ();
}
[Fact]
public void Begin_Null_Runnable_Throws ()
{
IApplication app = Application.Create ();
app.Init ("fake");
// Test null Runnable
Assert.Throws<ArgumentNullException> (() => app.Begin (null!));
app.Dispose ();
}
[Fact]
public void Begin_Sets_Application_Top_To_Console_Size ()
{
IApplication app = Application.Create ();
app.Init ("fake");
Assert.Null (app.TopRunnableView);
app.Driver!.SetScreenSize (80, 25);
Runnable top = new ();
SessionToken? token = app.Begin (top);
Assert.Equal (new (0, 0, 80, 25), app.TopRunnableView!.Frame);
app.Driver!.SetScreenSize (5, 5);
app.LayoutAndDraw ();
Assert.Equal (new (0, 0, 5, 5), app.TopRunnableView!.Frame);
if (token is { })
{
app.End (token);
}
top.Dispose ();
app.Dispose ();
}
[Fact]
public void Begin_WithNullRunnable_ThrowsArgumentNullException ()
{

View File

@@ -1,7 +1,3 @@
#nullable enable
using System;
using Terminal.Gui.App;
using Xunit;
namespace ApplicationTests;
public class ResultEventArgsTests
@@ -9,7 +5,7 @@ public class ResultEventArgsTests
[Fact]
public void DefaultConstructor_InitializesProperties ()
{
var args = new ResultEventArgs<string> ();
ResultEventArgs<string> args = new ();
Assert.Null (args.Result);
Assert.False (args.Handled);
@@ -18,7 +14,7 @@ public class ResultEventArgsTests
[Fact]
public void Constructor_WithResult_SetsResult ()
{
var args = new ResultEventArgs<int> (42);
ResultEventArgs<int> args = new (42);
Assert.Equal (42, args.Result);
Assert.False (args.Handled);
@@ -27,7 +23,7 @@ public class ResultEventArgsTests
[Fact]
public void Constructor_WithNullResult_AllowsNull ()
{
var args = new ResultEventArgs<string?> (null);
ResultEventArgs<string?> args = new (null);
Assert.Null (args.Result);
Assert.False (args.Handled);
@@ -36,7 +32,7 @@ public class ResultEventArgsTests
[Fact]
public void Result_CanBeSetAndRetrieved ()
{
var args = new ResultEventArgs<string> ();
ResultEventArgs<string> args = new ();
args.Result = "foo";
Assert.Equal ("foo", args.Result);
@@ -48,7 +44,7 @@ public class ResultEventArgsTests
[Fact]
public void Handled_CanBeSetAndRetrieved ()
{
var args = new ResultEventArgs<object> ();
ResultEventArgs<object> args = new ();
Assert.False (args.Handled);
args.Handled = true;
@@ -61,7 +57,7 @@ public class ResultEventArgsTests
[Fact]
public void WorksWithValueTypes ()
{
var args = new ResultEventArgs<int> ();
ResultEventArgs<int> args = new ();
Assert.Equal (0, args.Result); // default(int) is 0
args.Result = 123;
@@ -72,7 +68,7 @@ public class ResultEventArgsTests
public void WorksWithReferenceTypes ()
{
var obj = new object ();
var args = new ResultEventArgs<object> (obj);
ResultEventArgs<object> args = new (obj);
Assert.Same (obj, args.Result);
@@ -87,7 +83,8 @@ public class ResultEventArgsTests
public void EventHandler_CanChangeResult_AndCallerSeesChange ()
{
// Arrange
var args = new ResultEventArgs<string> ("initial");
ResultEventArgs<string> args = new ("initial");
StringResultEvent += (sender, e) =>
{
// Handler changes the result
@@ -101,17 +98,12 @@ public class ResultEventArgsTests
Assert.Equal ("changed by handler", args.Result);
}
[Fact]
public void EventHandler_CanSetResultToNull ()
{
// Arrange
var args = new ResultEventArgs<string> ("not null");
StringResultEvent += (sender, e) =>
{
e.Result = null;
};
ResultEventArgs<string> args = new ("not null");
StringResultEvent += (sender, e) => { e.Result = null; };
// Act
StringResultEvent?.Invoke (this, args);
@@ -124,7 +116,7 @@ public class ResultEventArgsTests
public void MultipleHandlers_LastHandlerWins ()
{
// Arrange
var args = new ResultEventArgs<int> (1);
ResultEventArgs<int> args = new (1);
EventHandler<ResultEventArgs<int>>? intEvent = null;
intEvent += (s, e) => e.Result = 2;
intEvent += (s, e) => e.Result = 3;
@@ -141,7 +133,7 @@ public class ResultEventArgsTests
public void EventHandler_CanChangeResult_Int ()
{
EventHandler<ResultEventArgs<int>> handler = (s, e) => e.Result = 99;
var args = new ResultEventArgs<int> (1);
ResultEventArgs<int> args = new (1);
handler.Invoke (this, args);
Assert.Equal (99, args.Result);
}
@@ -151,7 +143,7 @@ public class ResultEventArgsTests
public void EventHandler_CanChangeResult_Double ()
{
EventHandler<ResultEventArgs<double>> handler = (s, e) => e.Result = 2.718;
var args = new ResultEventArgs<double> (3.14);
ResultEventArgs<double> args = new (3.14);
handler.Invoke (this, args);
Assert.Equal (2.718, args.Result);
}
@@ -161,29 +153,39 @@ public class ResultEventArgsTests
public void EventHandler_CanChangeResult_Bool ()
{
EventHandler<ResultEventArgs<bool>> handler = (s, e) => e.Result = false;
var args = new ResultEventArgs<bool> (true);
ResultEventArgs<bool> args = new (true);
handler.Invoke (this, args);
Assert.False (args.Result);
}
// Enum
enum MyEnum { A, B, C }
private enum MyEnum
{
A,
B,
C
}
[Fact]
public void EventHandler_CanChangeResult_Enum ()
{
EventHandler<ResultEventArgs<MyEnum>> handler = (s, e) => e.Result = MyEnum.C;
var args = new ResultEventArgs<MyEnum> (MyEnum.A);
ResultEventArgs<MyEnum> args = new (MyEnum.A);
handler.Invoke (this, args);
Assert.Equal (MyEnum.C, args.Result);
}
// Struct
struct MyStruct { public int X; }
private struct MyStruct
{
public int X;
}
[Fact]
public void EventHandler_CanChangeResult_Struct ()
{
EventHandler<ResultEventArgs<MyStruct>> handler = (s, e) => e.Result = new MyStruct { X = 42 };
var args = new ResultEventArgs<MyStruct> (new MyStruct { X = 1 });
EventHandler<ResultEventArgs<MyStruct>> handler = (s, e) => e.Result = new() { X = 42 };
ResultEventArgs<MyStruct> args = new (new() { X = 1 });
handler.Invoke (this, args);
Assert.Equal (42, args.Result.X);
}
@@ -193,7 +195,7 @@ public class ResultEventArgsTests
public void EventHandler_CanChangeResult_String ()
{
EventHandler<ResultEventArgs<string>> handler = (s, e) => e.Result = "changed";
var args = new ResultEventArgs<string> ("original");
ResultEventArgs<string> args = new ("original");
handler.Invoke (this, args);
Assert.Equal ("changed", args.Result);
}
@@ -204,7 +206,7 @@ public class ResultEventArgsTests
{
var newObj = new object ();
EventHandler<ResultEventArgs<object>> handler = (s, e) => e.Result = newObj;
var args = new ResultEventArgs<object> (new object ());
ResultEventArgs<object> args = new (new ());
handler.Invoke (this, args);
Assert.Same (newObj, args.Result);
}
@@ -214,7 +216,7 @@ public class ResultEventArgsTests
public void EventHandler_CanChangeResult_NullableInt ()
{
EventHandler<ResultEventArgs<int?>> handler = (s, e) => e.Result = null;
var args = new ResultEventArgs<int?> (42);
ResultEventArgs<int?> args = new (42);
handler.Invoke (this, args);
Assert.Null (args.Result);
}
@@ -225,7 +227,7 @@ public class ResultEventArgsTests
{
var newArr = new [] { "x", "y" };
EventHandler<ResultEventArgs<string []>> handler = (s, e) => e.Result = newArr;
var args = new ResultEventArgs<string []> (new [] { "a", "b" });
ResultEventArgs<string []> args = new (new [] { "a", "b" });
handler.Invoke (this, args);
Assert.Equal (newArr, args.Result);
}
@@ -234,9 +236,9 @@ public class ResultEventArgsTests
[Fact]
public void EventHandler_CanChangeResult_List ()
{
var newList = new List<int> { 1, 2, 3 };
List<int> newList = new() { 1, 2, 3 };
EventHandler<ResultEventArgs<List<int>>> handler = (s, e) => e.Result = newList;
var args = new ResultEventArgs<List<int>> (new List<int> { 9 });
ResultEventArgs<List<int>> args = new (new() { 9 });
handler.Invoke (this, args);
Assert.Equal (newList, args.Result);
}
@@ -245,21 +247,22 @@ public class ResultEventArgsTests
[Fact]
public void EventHandler_CanChangeResult_Dictionary ()
{
var newDict = new Dictionary<string, int> { ["a"] = 1 };
Dictionary<string, int> newDict = new() { ["a"] = 1 };
EventHandler<ResultEventArgs<Dictionary<string, int>>> handler = (s, e) => e.Result = newDict;
var args = new ResultEventArgs<Dictionary<string, int>> (new Dictionary<string, int> ());
ResultEventArgs<Dictionary<string, int>> args = new (new ());
handler.Invoke (this, args);
Assert.Equal (newDict, args.Result);
}
// Record
public record MyRecord (int Id, string Name);
[Fact]
public void EventHandler_CanChangeResult_Record ()
{
var rec = new MyRecord (1, "foo");
EventHandler<ResultEventArgs<MyRecord>> handler = (s, e) => e.Result = rec;
var args = new ResultEventArgs<MyRecord> (null);
ResultEventArgs<MyRecord> args = new (null);
handler.Invoke (this, args);
Assert.Equal (rec, args.Result);
}
@@ -269,12 +272,12 @@ public class ResultEventArgsTests
public void EventHandler_CanChangeResult_NullableInt_ToValue_AndNull ()
{
EventHandler<ResultEventArgs<int?>> handler = (s, e) => e.Result = 123;
var args = new ResultEventArgs<int?> (null);
ResultEventArgs<int?> args = new (null);
handler.Invoke (this, args);
Assert.Equal (123, args.Result);
handler = (s, e) => e.Result = null;
args = new ResultEventArgs<int?> (456);
args = new (456);
handler.Invoke (this, args);
Assert.Null (args.Result);
}
@@ -284,12 +287,12 @@ public class ResultEventArgsTests
public void EventHandler_CanChangeResult_NullableDouble_ToValue_AndNull ()
{
EventHandler<ResultEventArgs<double?>> handler = (s, e) => e.Result = 3.14;
var args = new ResultEventArgs<double?> (null);
ResultEventArgs<double?> args = new (null);
handler.Invoke (this, args);
Assert.Equal (3.14, args.Result);
handler = (s, e) => e.Result = null;
args = new ResultEventArgs<double?> (2.71);
args = new (2.71);
handler.Invoke (this, args);
Assert.Null (args.Result);
}
@@ -299,12 +302,12 @@ public class ResultEventArgsTests
public void EventHandler_CanChangeResult_NullableStruct_ToValue_AndNull ()
{
EventHandler<ResultEventArgs<MyStruct?>> handler = (s, e) => e.Result = new MyStruct { X = 7 };
var args = new ResultEventArgs<MyStruct?> (null);
ResultEventArgs<MyStruct?> args = new (null);
handler.Invoke (this, args);
Assert.Equal (7, args.Result?.X);
handler = (s, e) => e.Result = null;
args = new ResultEventArgs<MyStruct?> (new MyStruct { X = 8 });
args = new (new MyStruct { X = 8 });
handler.Invoke (this, args);
Assert.Null (args.Result);
}
@@ -314,29 +317,33 @@ public class ResultEventArgsTests
public void EventHandler_CanChangeResult_NullableString_ToValue_AndNull ()
{
EventHandler<ResultEventArgs<string?>> handler = (s, e) => e.Result = "hello";
var args = new ResultEventArgs<string?> (null);
ResultEventArgs<string?> args = new (null);
handler.Invoke (this, args);
Assert.Equal ("hello", args.Result);
handler = (s, e) => e.Result = null;
args = new ResultEventArgs<string?> ("world");
args = new ("world");
handler.Invoke (this, args);
Assert.Null (args.Result);
}
// Nullable custom class
class MyClass { public int Y { get; set; } }
private class MyClass
{
public int Y { get; set; }
}
[Fact]
public void EventHandler_CanChangeResult_NullableClass_ToValue_AndNull ()
{
EventHandler<ResultEventArgs<MyClass?>> handler = (s, e) => e.Result = new MyClass { Y = 42 };
var args = new ResultEventArgs<MyClass?> (null);
EventHandler<ResultEventArgs<MyClass?>> handler = (s, e) => e.Result = new() { Y = 42 };
ResultEventArgs<MyClass?> args = new (null);
handler.Invoke (this, args);
Assert.NotNull (args.Result);
Assert.Equal (42, args.Result?.Y);
handler = (s, e) => e.Result = null;
args = new ResultEventArgs<MyClass?> (new MyClass { Y = 99 });
args = new (new() { Y = 99 });
handler.Invoke (this, args);
Assert.Null (args.Result);
}

View File

@@ -0,0 +1,170 @@
using Xunit.Abstractions;
namespace ApplicationTests.Init;
/// <summary>
/// Comprehensive tests for ApplicationImpl.Begin/End logic that manages Current and SessionStack.
/// These tests ensure the fragile state management logic is robust and catches regressions.
/// Tests work directly with ApplicationImpl instances to avoid global Application state issues.
/// </summary>
public class InitTests (ITestOutputHelper output)
{
private readonly ITestOutputHelper _output = output;
[Fact]
public void Init_Unbalanced_Throws ()
{
IApplication? app = Application.Create ();
app.Init ("fake");
Assert.Throws<InvalidOperationException> (() =>
app.Init ("fake")
);
}
[Fact]
public void Init_Null_Driver_Should_Pick_A_Driver ()
{
IApplication app = Application.Create ();
app.Init ();
Assert.NotNull (app.Driver);
app.Dispose ();
}
[Fact]
public void Init_Dispose_Cleans_Up ()
{
IApplication app = Application.Create ();
app.Init ("fake");
app.Dispose ();
#if DEBUG_IDISPOSABLE
// Validate there are no outstanding Responder-based instances
// after cleanup
// Note: We can't check View.Instances in parallel tests as it's a static field
// that would be shared across parallel test runs
#endif
}
[Fact]
public void Init_Dispose_Fire_InitializedChanged ()
{
var initialized = false;
var Dispose = false;
IApplication app = Application.Create ();
app.InitializedChanged += OnApplicationOnInitializedChanged;
app.Init (driverName: "fake");
Assert.True (initialized);
Assert.False (Dispose);
app.Dispose ();
Assert.True (initialized);
Assert.True (Dispose);
app.InitializedChanged -= OnApplicationOnInitializedChanged;
return;
void OnApplicationOnInitializedChanged (object? s, EventArgs<bool> a)
{
if (a.Value)
{
initialized = true;
}
else
{
Dispose = true;
}
}
}
[Fact]
public void Init_KeyBindings_Are_Not_Reset ()
{
IApplication app = Application.Create ();
// Set via Keyboard property (modern API)
app.Keyboard.QuitKey = Key.Q;
Assert.Equal (Key.Q, app.Keyboard.QuitKey);
app.Init ("fake");
Assert.Equal (Key.Q, app.Keyboard.QuitKey);
app.Dispose ();
}
[Fact]
public void Init_NoParam_ForceDriver_Works ()
{
using IApplication app = Application.Create ();
app.ForceDriver = "fake";
// Note: Init() without params picks up driver configuration
app.Init ();
Assert.Equal ("fake", app.Driver!.GetName ());
}
[Fact]
public void Init_Dispose_Resets_Instance_Properties ()
{
IApplication app = Application.Create ();
// Init the app
app.Init (driverName: "fake");
// Verify initialized
Assert.True (app.Initialized);
Assert.NotNull (app.Driver);
// Dispose cleans up
app.Dispose ();
// Check reset state on the instance
CheckReset (app);
// Create a new instance and set values
app = Application.Create ();
app.Init ("fake");
app.StopAfterFirstIteration = true;
app.Keyboard.PrevTabGroupKey = Key.A;
app.Keyboard.NextTabGroupKey = Key.B;
app.Keyboard.QuitKey = Key.C;
app.Keyboard.KeyBindings.Add (Key.D, Command.Cancel);
app.Mouse.CachedViewsUnderMouse.Clear ();
app.Mouse.LastMousePosition = new Point (1, 1);
// Dispose and check reset
app.Dispose ();
CheckReset (app);
return;
void CheckReset (IApplication application)
{
// Check that all fields and properties are reset on the instance
// Public Properties
Assert.Null (application.TopRunnableView);
Assert.Null (application.Mouse.MouseGrabView);
Assert.Null (application.Driver);
Assert.False (application.StopAfterFirstIteration);
// Internal properties
Assert.False (application.Initialized);
Assert.Null (application.MainThreadId);
Assert.Empty (application.Mouse.CachedViewsUnderMouse);
}
}
}

View File

@@ -1,7 +1,7 @@
// ReSharper disable AccessToDisposedClosure
#nullable enable
namespace ApplicationTests;
namespace ApplicationTests.Keyboard;
/// <summary>
/// Tests to verify that KeyboardImpl is thread-safe for concurrent access scenarios.

View File

@@ -1,7 +1,7 @@
#nullable enable
using Terminal.Gui.App;
namespace ApplicationTests;
namespace ApplicationTests.Keyboard;
/// <summary>
/// Parallelizable tests for keyboard handling.
@@ -9,6 +9,23 @@ namespace ApplicationTests;
/// </summary>
public class KeyboardTests
{
[Fact]
public void Init_CreatesKeybindings ()
{
IApplication app = Application.Create ();
app.Keyboard.KeyBindings.Clear ();
Assert.Empty (app.Keyboard.KeyBindings.GetBindings ());
app.Init ("fake");
Assert.NotEmpty (app.Keyboard.KeyBindings.GetBindings ());
app.Dispose ();
}
[Fact]
public void Constructor_InitializesKeyBindings ()
{
@@ -245,7 +262,7 @@ public class KeyboardTests
}
// Migrated from UnitTests/Application/KeyboardTests.cs
[Fact]
public void KeyBindings_Add_Adds ()
{
@@ -465,7 +482,7 @@ public class KeyboardTests
// Get the commands from the old binding
Assert.True (keyboard.KeyBindings.TryGet (oldKey, out KeyBinding oldBinding));
Command[] oldCommands = oldBinding.Commands.ToArray ();
Command [] oldCommands = oldBinding.Commands.ToArray ();
// Act
keyboard.KeyBindings.Replace (oldKey, newKey);

View File

@@ -1,7 +1,7 @@
#nullable enable
using System.ComponentModel;
namespace ApplicationTests;
namespace ApplicationTests.Mouse;
[Trait ("Category", "Input")]
public class ApplicationMouseEnterLeaveTests

View File

@@ -1,8 +1,6 @@
#nullable enable
using Terminal.Gui.App;
using Xunit.Abstractions;
namespace ApplicationTests;
namespace ApplicationTests.Mouse;
/// <summary>
/// Parallelizable tests for IMouse interface.
@@ -93,14 +91,14 @@ public class MouseInterfaceTests (ITestOutputHelper output)
MouseEventArgs? capturedArgs = null;
mouse.MouseEvent += (sender, args) =>
{
eventFired = true;
capturedArgs = args;
};
{
eventFired = true;
capturedArgs = args;
};
MouseEventArgs testEvent = new ()
{
ScreenPosition = new Point (5, 10),
ScreenPosition = new (5, 10),
Flags = MouseFlags.Button1Pressed
};
@@ -121,13 +119,13 @@ public class MouseInterfaceTests (ITestOutputHelper output)
MouseImpl mouse = new ();
var eventCount = 0;
void Handler (object? sender, MouseEventArgs args) => eventCount++;
void Handler (object? sender, MouseEventArgs args) { eventCount++; }
mouse.MouseEvent += Handler;
MouseEventArgs testEvent = new ()
{
ScreenPosition = new Point (0, 0),
ScreenPosition = new (0, 0),
Flags = MouseFlags.Button1Pressed
};
@@ -157,7 +155,7 @@ public class MouseInterfaceTests (ITestOutputHelper output)
MouseEventArgs testEvent = new ()
{
ScreenPosition = new Point (0, 0),
ScreenPosition = new (0, 0),
Flags = MouseFlags.Button1Pressed
};
@@ -185,7 +183,7 @@ public class MouseInterfaceTests (ITestOutputHelper output)
MouseEventArgs testEvent = new ()
{
ScreenPosition = new Point (5, 5),
ScreenPosition = new (5, 5),
Flags = flags
};
@@ -231,7 +229,7 @@ public class MouseInterfaceTests (ITestOutputHelper output)
MouseEventArgs testEvent = new ()
{
ScreenPosition = new Point (0, 0),
ScreenPosition = new (0, 0),
Flags = MouseFlags.Button1Pressed
};
@@ -300,7 +298,7 @@ public class MouseInterfaceTests (ITestOutputHelper output)
MouseEventArgs testEvent = new ()
{
ScreenPosition = new Point (0, 0),
ScreenPosition = new (0, 0),
Flags = MouseFlags.Button1Pressed
};
@@ -380,10 +378,10 @@ public class MouseInterfaceTests (ITestOutputHelper output)
var eventFired = false;
mouse.GrabbingMouse += (sender, args) =>
{
eventFired = true;
args.Cancel = true;
};
{
eventFired = true;
args.Cancel = true;
};
// Act
mouse.GrabMouse (testView);
@@ -403,10 +401,10 @@ public class MouseInterfaceTests (ITestOutputHelper output)
View? eventView = null;
mouse.GrabbedMouse += (sender, args) =>
{
eventFired = true;
eventView = args.View;
};
{
eventFired = true;
eventView = args.View;
};
// Act
mouse.GrabMouse (testView);
@@ -428,10 +426,10 @@ public class MouseInterfaceTests (ITestOutputHelper output)
View? eventView = null;
mouse.UnGrabbedMouse += (sender, args) =>
{
eventFired = true;
eventView = args.View;
};
{
eventFired = true;
eventView = args.View;
};
// Act
mouse.UngrabMouse ();

View File

@@ -1,6 +1,4 @@
using Xunit.Abstractions;
namespace ApplicationTests;
namespace ApplicationTests.Mouse;
/// <summary>
/// Tests for the <see cref="IMouse"/> interface and <see cref="MouseImpl"/> implementation.

View File

@@ -2,7 +2,7 @@
using Moq;
using Terminal.Gui.App;
namespace ApplicationTests;
namespace ApplicationTests.Popover;
public class ApplicationPopoverTests
{

View File

@@ -1,13 +1,10 @@
using System;
using Terminal.Gui;
using Terminal.Gui.App;
using Xunit;
namespace ApplicationTests;
namespace ApplicationTests.Popover;
public class PopoverBaseImplTests
{
// Minimal concrete implementation for testing
private class TestPopover : PopoverBaseImpl { }
private class TestPopover : PopoverBaseImpl
{ }
[Fact]
public void Constructor_SetsDefaults ()
@@ -40,12 +37,11 @@ public class PopoverBaseImplTests
popover.ViewportSettings = ViewportSettingsFlags.None; // Remove required flags
var popoverManager = new ApplicationPopover ();
// Test missing Transparent flags
Assert.ThrowsAny<Exception> (() => popoverManager.Show (popover));
}
[Fact]
public void Show_ThrowsIfPopoverMissingQuitCommand ()
{

View File

@@ -0,0 +1,252 @@
#nullable enable
using Xunit.Abstractions;
namespace ApplicationTests;
public class RunTests
{
[Fact]
public void Run_RequestStop_Stops ()
{
IApplication? app = Application.Create ();
app.Init ("fake");
var top = new Runnable ();
SessionToken? sessionToken = app.Begin (top);
Assert.NotNull (sessionToken);
app.Iteration += OnApplicationOnIteration;
app.Run (top);
app.Iteration -= OnApplicationOnIteration;
top.Dispose ();
return;
void OnApplicationOnIteration (object? s, EventArgs<IApplication?> a) { app.RequestStop (); }
}
[Fact]
public void Run_T_Init_Driver_Cleared_with_Runnable_Throws ()
{
IApplication? app = Application.Create ();
app.Init ("fake");
app.Driver = null;
app.StopAfterFirstIteration = true;
// Init has been called, but Driver has been set to null. Bad.
Assert.Throws<InvalidOperationException> (() => app.Run<Runnable> ());
}
[Fact]
public void Run_Iteration_Fires ()
{
var iteration = 0;
IApplication app = Application.Create ();
app.Init ("fake");
app.Iteration += Application_Iteration;
app.Run<Runnable> ();
app.Iteration -= Application_Iteration;
Assert.Equal (1, iteration);
app.Dispose ();
return;
void Application_Iteration (object? sender, EventArgs<IApplication?> e)
{
iteration++;
app.RequestStop ();
}
}
[Fact]
public void Run_T_After_InitWithDriver_with_Runnable_and_Driver_Does_Not_Throw ()
{
IApplication app = Application.Create ();
app.StopAfterFirstIteration = true;
// Run<Runnable<bool>> when already initialized or not with a Driver will not throw (because Window is derived from Runnable)
// Using another type not derived from Runnable will throws at compile time
app.Run<Window> (null, "fake");
// Run<Runnable<bool>> when already initialized or not with a Driver will not throw (because Dialog is derived from Runnable)
app.Run<Dialog> (null, "fake");
app.Dispose ();
}
[Fact]
public void Run_T_After_Init_Does_Not_Disposes_Application_Top ()
{
IApplication app = Application.Create ();
app.Init ("fake");
// Init doesn't create a Runnable and assigned it to app.TopRunnable
// but Begin does
var initTop = new Runnable ();
app.Iteration += OnApplicationOnIteration;
app.Run<Runnable> ();
app.Iteration -= OnApplicationOnIteration;
#if DEBUG_IDISPOSABLE
Assert.False (initTop.WasDisposed);
initTop.Dispose ();
Assert.True (initTop.WasDisposed);
#endif
initTop.Dispose ();
app.Dispose ();
return;
void OnApplicationOnIteration (object? s, EventArgs<IApplication?> a)
{
Assert.NotEqual (initTop, app.TopRunnableView);
#if DEBUG_IDISPOSABLE
Assert.False (initTop.WasDisposed);
#endif
app.RequestStop ();
}
}
[Fact]
public void Run_T_After_InitWithDriver_with_TestRunnable_DoesNotThrow ()
{
IApplication app = Application.Create ();
app.Init ("fake");
app.StopAfterFirstIteration = true;
// Init has been called and we're passing no driver to Run<TestRunnable>. This is ok.
app.Run<Window> ();
app.Dispose ();
}
[Fact]
public void Run_T_After_InitNullDriver_with_TestRunnable_DoesNotThrow ()
{
IApplication app = Application.Create ();
app.Init ("fake");
app.StopAfterFirstIteration = true;
// Init has been called, selecting FakeDriver; we're passing no driver to Run<TestRunnable>. Should be fine.
app.Run<Window> ();
app.Dispose ();
}
[Fact]
public void Run_T_NoInit_DoesNotThrow ()
{
IApplication app = Application.Create ();
app.StopAfterFirstIteration = true;
app.Run<Window> ();
app.Dispose ();
}
[Fact]
public void Run_T_NoInit_WithDriver_DoesNotThrow ()
{
IApplication app = Application.Create ();
app.StopAfterFirstIteration = true;
// Init has NOT been called and we're passing a valid driver to Run<TestRunnable>. This is ok.
app.Run<Runnable> (null, "fake");
app.Dispose ();
}
[Fact]
public void Run_Sets_Running_True ()
{
IApplication app = Application.Create ();
app.Init ("fake");
var top = new Runnable ();
SessionToken? rs = app.Begin (top);
Assert.NotNull (rs);
app.Iteration += OnApplicationOnIteration;
app.Run (top);
app.Iteration -= OnApplicationOnIteration;
top.Dispose ();
app.Dispose ();
return;
void OnApplicationOnIteration (object? s, EventArgs<IApplication?> a)
{
Assert.True (top.IsRunning);
top.RequestStop ();
}
}
[Fact]
public void Run_A_Modal_Runnable_Refresh_Background_On_Moving ()
{
IApplication app = Application.Create ();
app.Init ("fake");
// Don't use Dialog here as it has more layout logic. Use Window instead.
var w = new Window
{
Width = 5, Height = 5,
Arrangement = ViewArrangement.Movable
};
app.Driver!.SetScreenSize (10, 10);
SessionToken? rs = app.Begin (w);
// Don't use visuals to test as style of border can change over time.
Assert.Equal (new (0, 0), w.Frame.Location);
app.Mouse.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed });
Assert.Equal (w.Border, app.Mouse.MouseGrabView);
Assert.Equal (new (0, 0), w.Frame.Location);
// Move down and to the right.
app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (1, 1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition });
Assert.Equal (new (1, 1), w.Frame.Location);
app.End (rs!);
w.Dispose ();
app.Dispose ();
}
[Fact]
public void Run_T_Creates_Top_Without_Init ()
{
IApplication app = Application.Create ();
app.StopAfterFirstIteration = true;
app.SessionEnded += OnApplicationOnSessionEnded;
app.Run<Window> (null, "fake");
Assert.Null (app.TopRunnableView);
app.Dispose ();
Assert.Null (app.TopRunnableView);
return;
void OnApplicationOnSessionEnded (object? sender, SessionTokenEventArgs e)
{
app.SessionEnded -= OnApplicationOnSessionEnded;
e.State.Result = (e.State.Runnable as IRunnable<object?>)?.Result;
}
}
}

View File

@@ -1,7 +1,6 @@
#nullable enable
using Xunit.Abstractions;
namespace ApplicationTests;
namespace ApplicationTests.RunnableTests;
/// <summary>
/// Tests for edge cases and error conditions in IRunnable implementation.
@@ -9,7 +8,7 @@ namespace ApplicationTests;
public class RunnableEdgeCasesTests (ITestOutputHelper output)
{
private readonly ITestOutputHelper _output = output;
[Fact]
public void Runnable_MultipleEventSubscribers_AllInvoked ()
{

View File

@@ -1,28 +1,19 @@
#nullable enable
using Xunit.Abstractions;
namespace ApplicationTests;
namespace ApplicationTests.RunnableTests;
/// <summary>
/// Integration tests for IApplication's IRunnable support.
/// Tests the full lifecycle of IRunnable instances through Application methods.
/// </summary>
public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : IDisposable
public class ApplicationRunnableIntegrationTests
{
private readonly ITestOutputHelper _output = output;
private IApplication? _app;
public void Dispose ()
{
_app?.Dispose ();
_app = null;
}
[Fact]
public void Begin_AddsRunnableToStack ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
Runnable<int> runnable = new ();
int stackCountBefore = app.SessionStack?.Count ?? 0;
@@ -43,7 +34,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void Begin_CanBeCanceled_ByIsRunningChanging ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
CancelableRunnable runnable = new () { CancelStart = true };
// Act
@@ -60,7 +51,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void Begin_RaisesIsModalChangedEvent ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
Runnable<int> runnable = new ();
var isModalChangedRaised = false;
bool? receivedValue = null;
@@ -86,7 +77,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void Begin_RaisesIsRunningChangedEvent ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
Runnable<int> runnable = new ();
var isRunningChangedRaised = false;
bool? receivedValue = null;
@@ -112,7 +103,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void Begin_RaisesIsRunningChangingEvent ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
Runnable<int> runnable = new ();
var isRunningChangingRaised = false;
bool? oldValue = null;
@@ -141,7 +132,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void Begin_SetsIsModalToTrue ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
Runnable<int> runnable = new ();
// Act
@@ -158,7 +149,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void Begin_SetsIsRunningToTrue ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
Runnable<int> runnable = new ();
// Act
@@ -175,7 +166,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void Begin_ThrowsOnNullRunnable ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
// Act & Assert
Assert.Throws<ArgumentNullException> (() => app.Begin ((IRunnable)null!));
@@ -185,7 +176,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void End_CanBeCanceled_ByIsRunningChanging ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
CancelableRunnable runnable = new () { CancelStop = true };
SessionToken? token = app.Begin (runnable);
runnable.CancelStop = true; // Enable cancellation
@@ -205,7 +196,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void End_ClearsTokenRunnable ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
Runnable<int> runnable = new ();
SessionToken? token = app.Begin (runnable);
@@ -220,7 +211,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void End_RaisesIsRunningChangedEvent ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
Runnable<int> runnable = new ();
SessionToken? token = app.Begin (runnable);
var isRunningChangedRaised = false;
@@ -244,7 +235,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void End_RaisesIsRunningChangingEvent ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
Runnable<int> runnable = new ();
SessionToken? token = app.Begin (runnable);
var isRunningChangingRaised = false;
@@ -271,7 +262,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void End_RemovesRunnableFromStack ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
Runnable<int> runnable = new ();
SessionToken? token = app.Begin (runnable);
int stackCountBefore = app.SessionStack?.Count ?? 0;
@@ -287,7 +278,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void End_SetsIsModalToFalse ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
Runnable<int> runnable = new ();
SessionToken? token = app.Begin (runnable);
@@ -302,7 +293,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void End_SetsIsRunningToFalse ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
Runnable<int> runnable = new ();
SessionToken? token = app.Begin (runnable);
@@ -317,17 +308,33 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void End_ThrowsOnNullToken ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
// Act & Assert
Assert.Throws<ArgumentNullException> (() => app.End ((SessionToken)null!));
}
[Fact]
public void End_ClearsMouseGrabView ()
{
// Arrange
IApplication app = CreateAndInitApp ();
Runnable<int> runnable = new ();
SessionToken? token = app.Begin (runnable);
app.Mouse.GrabMouse (runnable);
app.End (token!);
Assert.Null (app.Mouse.MouseGrabView);
runnable.Dispose ();
app.Dispose ();
}
[Fact]
public void MultipleRunnables_IndependentResults ()
{
// Arrange
IApplication app = GetApp ();
Runnable<int> runnable1 = new ();
Runnable<string> runnable2 = new ();
@@ -344,7 +351,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void NestedBegin_MaintainsStackOrder ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
Runnable<int> runnable1 = new () { Id = "1" };
Runnable<int> runnable2 = new () { Id = "2" };
@@ -367,7 +374,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void NestedEnd_RestoresPreviousModal ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
Runnable<int> runnable1 = new () { Id = "1" };
Runnable<int> runnable2 = new () { Id = "2" };
SessionToken token1 = app.Begin (runnable1)!;
@@ -390,7 +397,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void RequestStop_WithIRunnable_WorksCorrectly ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
StoppableRunnable runnable = new ();
SessionToken? token = app.Begin (runnable);
@@ -409,7 +416,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void RequestStop_WithNull_UsesTopRunnable ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
StoppableRunnable runnable = new ();
SessionToken? token = app.Begin (runnable);
@@ -427,7 +434,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
public void RunGeneric_CreatesAndReturnsRunnable ()
{
// Arrange
IApplication app = GetApp ();
IApplication app = CreateAndInitApp ();
app.StopAfterFirstIteration = true;
// Act - With fluent API, Run<T>() returns IApplication for chaining
@@ -456,15 +463,12 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID
app.Dispose ();
}
private IApplication GetApp ()
private IApplication CreateAndInitApp ()
{
if (_app is null)
{
_app = Application.Create ();
_app.Init ("fake");
}
IApplication app = Application.Create ();
app.Init ("fake");
return _app;
return app;
}
/// <summary>

View File

@@ -1,17 +1,44 @@
using Xunit.Abstractions;
namespace ApplicationTests;
namespace ApplicationTests.Screen;
/// <summary>
/// Parallelizable tests for IApplication.ScreenChanged event and Screen property.
/// Tests using the modern instance-based IApplication API.
/// </summary>
public class IApplicationScreenChangedTests (ITestOutputHelper output)
public class ScreenTests (ITestOutputHelper output)
{
private readonly ITestOutputHelper _output = output;
#region ScreenChanged Event Tests
[Fact]
public void Screen_Size_Changes ()
{
IApplication app = Application.Create ();
app.Init ("fake");
IDriver? driver = app.Driver;
app.Driver!.SetScreenSize (80, 25);
Assert.Equal (new (0, 0, 80, 25), driver!.Screen);
Assert.Equal (new (0, 0, 80, 25), app.Screen);
// TODO: Should not be possible to manually change these at whim!
driver.Cols = 100;
driver.Rows = 30;
app.Driver!.SetScreenSize (100, 30);
Assert.Equal (new (0, 0, 100, 30), driver.Screen);
app.Screen = new (0, 0, driver.Cols, driver.Rows);
Assert.Equal (new (0, 0, 100, 30), driver.Screen);
app.Dispose ();
}
[Fact]
public void ScreenChanged_Event_Fires_When_Driver_Size_Changes ()
{

View File

@@ -1,4 +1,4 @@
namespace ApplicationTests;
namespace ApplicationTests.Timeout;
public class LogarithmicTimeoutTests
{

View File

@@ -1,5 +1,4 @@
namespace ApplicationTests;
namespace ApplicationTests.Timeout;
public class SmoothAcceleratingTimeoutTests
{

View File

@@ -853,4 +853,23 @@ public class TimeoutTests
}
}
}
[Fact]
public void Invoke_Adds_Idle ()
{
IApplication app = Application.Create ();
app.Init ("fake");
Runnable top = new ();
SessionToken? rs = app.Begin (top);
var actionCalled = 0;
app.Invoke ((_) => { actionCalled++; });
app.TimedEvents!.RunTimers ();
Assert.Equal (1, actionCalled);
top.Dispose ();
app.Dispose ();
}
}

View File

@@ -1,7 +1,7 @@
using Terminal.Gui.App;
using Xunit.Abstractions;
namespace ApplicationTests;
namespace ApplicationTests.Mouse;
/// <summary>
/// Parallelizable tests for mouse event routing and coordinate transformation.
@@ -283,10 +283,10 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
#region Mouse Button Events
[Theory]
[InlineData (MouseFlags.Button1Pressed, 1, 0, 0)]
[InlineData (MouseFlags.Button1Released, 0, 1, 0)]
[InlineData (MouseFlags.Button1Clicked, 0, 0, 1)]
public void View_MouseButtonEvents_RaiseCorrectHandlers (MouseFlags flags, int expectedPressed, int expectedReleased, int expectedClicked)
[InlineData (MouseFlags.Button1Pressed, 1, 0)]
[InlineData (MouseFlags.Button1Released, 0, 1)]
[InlineData (MouseFlags.Button1Clicked, 0, 0)]
public void View_MouseButtonEvents_RaiseCorrectHandlers (MouseFlags flags, int expectedPressed, int expectedReleased)
{
// Arrange
View view = new () { Width = 10, Height = 10 };