Merge branch 'v2_develop' into copilot/enable-menubar-replacement

This commit is contained in:
Tig
2025-12-08 12:52:58 -07:00
committed by GitHub
81 changed files with 2870 additions and 1791 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.
@@ -128,46 +126,46 @@ public class MouseTests
[Theory]
// click on border
[InlineData (0, 0, 0, 0, 0, false)]
[InlineData (0, 1, 0, 0, 0, false)]
[InlineData (0, 0, 1, 0, 0, false)]
[InlineData (0, 9, 0, 0, 0, false)]
[InlineData (0, 0, 9, 0, 0, false)]
[InlineData (0, 0, 0, 0, 0, 0)]
[InlineData (0, 1, 0, 0, 0, 0)]
[InlineData (0, 0, 1, 0, 0, 0)]
[InlineData (0, 9, 0, 0, 0, 0)]
[InlineData (0, 0, 9, 0, 0, 0)]
// outside border
[InlineData (0, 10, 0, 0, 0, false)]
[InlineData (0, 0, 10, 0, 0, false)]
[InlineData (0, 10, 0, 0, 0, 0)]
[InlineData (0, 0, 10, 0, 0, 0)]
// view is offset from origin ; click is on border
[InlineData (1, 1, 1, 0, 0, false)]
[InlineData (1, 2, 1, 0, 0, false)]
[InlineData (1, 1, 2, 0, 0, false)]
[InlineData (1, 10, 1, 0, 0, false)]
[InlineData (1, 1, 10, 0, 0, false)]
[InlineData (1, 1, 1, 0, 0, 0)]
[InlineData (1, 2, 1, 0, 0, 0)]
[InlineData (1, 1, 2, 0, 0, 0)]
[InlineData (1, 10, 1, 0, 0, 0)]
[InlineData (1, 1, 10, 0, 0, 0)]
// outside border
[InlineData (1, -1, 0, 0, 0, false)]
[InlineData (1, 0, -1, 0, 0, false)]
[InlineData (1, 10, 10, 0, 0, false)]
[InlineData (1, 11, 11, 0, 0, false)]
[InlineData (1, -1, 0, 0, 0, 0)]
[InlineData (1, 0, -1, 0, 0, 0)]
[InlineData (1, 10, 10, 0, 0, 0)]
[InlineData (1, 11, 11, 0, 0, 0)]
// view is at origin, click is inside border
[InlineData (0, 1, 1, 0, 0, true)]
[InlineData (0, 2, 1, 1, 0, true)]
[InlineData (0, 1, 2, 0, 1, true)]
[InlineData (0, 8, 1, 7, 0, true)]
[InlineData (0, 1, 8, 0, 7, true)]
[InlineData (0, 8, 8, 7, 7, true)]
[InlineData (0, 1, 1, 0, 0, 1)]
[InlineData (0, 2, 1, 1, 0, 1)]
[InlineData (0, 1, 2, 0, 1, 1)]
[InlineData (0, 8, 1, 7, 0, 1)]
[InlineData (0, 1, 8, 0, 7, 1)]
[InlineData (0, 8, 8, 7, 7, 1)]
// view is offset from origin ; click inside border
// our view is 10x10, but has a border, so it's bounds is 8x8
[InlineData (1, 2, 2, 0, 0, true)]
[InlineData (1, 3, 2, 1, 0, true)]
[InlineData (1, 2, 3, 0, 1, true)]
[InlineData (1, 9, 2, 7, 0, true)]
[InlineData (1, 2, 9, 0, 7, true)]
[InlineData (1, 9, 9, 7, 7, true)]
[InlineData (1, 10, 10, 7, 7, false)]
[InlineData (1, 2, 2, 0, 0, 1)]
[InlineData (1, 3, 2, 1, 0, 1)]
[InlineData (1, 2, 3, 0, 1, 1)]
[InlineData (1, 9, 2, 7, 0, 1)]
[InlineData (1, 2, 9, 0, 7, 1)]
[InlineData (1, 9, 9, 7, 7, 1)]
[InlineData (1, 10, 10, 7, 7, 0)]
//01234567890123456789
// |12345678|
@@ -178,13 +176,13 @@ public class MouseTests
int clickY,
int expectedX,
int expectedY,
bool expectedClicked
int expectedClickedCount
)
{
Size size = new (10, 10);
Point pos = new (offset, offset);
var clicked = false;
int clickedCount = 0;
using IApplication? application = Application.Create ();
@@ -208,14 +206,14 @@ public class MouseTests
var mouseEvent = new MouseEventArgs { Position = new (clickX, clickY), ScreenPosition = new (clickX, clickY), Flags = MouseFlags.Button1Clicked };
view.MouseClick += (s, e) =>
view.MouseEvent += (_s, e) =>
{
Assert.Equal (expectedX, e.Position.X);
Assert.Equal (expectedY, e.Position.Y);
clicked = true;
clickedCount += e.IsSingleDoubleOrTripleClicked ? 1 : 0;
};
application.Mouse.RaiseMouseEvent (mouseEvent);
Assert.Equal (expectedClicked, clicked);
Assert.Equal (expectedClickedCount, clickedCount);
}
}

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

@@ -19,7 +19,7 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
List<string> executionOrder = new ();
var mainWindow = new Window { Title = "Main Window" };
var dialog = new Dialog { Title = "Nested Dialog", Buttons = [new() { Text = "Ok" }] };
var dialog = new Dialog { Title = "Nested Dialog", Buttons = [new () { Text = "Ok" }] };
var nestedRunCompleted = false;
// Use iteration counter for safety instead of time-based timeout
@@ -158,17 +158,17 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
var mainWindow = new Window { Title = "Main Window" };
// Create a dialog for the nested run loop
var dialog = new Dialog { Title = "Nested Dialog", Buttons = [new() { Text = "Ok" }] };
var dialog = new Dialog { Title = "Nested Dialog", Buttons = [new () { Text = "Ok" }] };
// Schedule a safety timeout that will ensure the app quits if test hangs
var requestStopTimeoutFired = false;
var safetyRequestStopTimeoutFired = false;
app.AddTimeout (
TimeSpan.FromMilliseconds (10000),
() =>
{
output.WriteLine ("SAFETY: RequestStop Timeout fired - test took too long!");
requestStopTimeoutFired = true;
output.WriteLine ("SAFETY: RequestStop Timeout fired - test took too long - Assuming slow environment. Skipping assertions.");
safetyRequestStopTimeoutFired = true;
app.RequestStop ();
return false;
@@ -217,12 +217,13 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
// Act - Start the main run loop
app.Run (mainWindow);
// Assert
Assert.True (nestedRunStarted, "Nested run should have started");
Assert.True (timeoutFired, "Timeout should have fired during nested run");
Assert.True (nestedRunEnded, "Nested run should have ended");
Assert.False (requestStopTimeoutFired, "Safety timeout should NOT have fired");
if (!safetyRequestStopTimeoutFired)
{
// Assert
Assert.True (nestedRunStarted, "Nested run should have started");
Assert.True (timeoutFired, "Timeout should have fired during nested run");
Assert.True (nestedRunEnded, "Nested run should have ended");
}
dialog.Dispose ();
mainWindow.Dispose ();
@@ -273,14 +274,14 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
app.Init ("FakeDriver");
// Schedule a safety timeout that will ensure the app quits if test hangs
var requestStopTimeoutFired = false;
var safetyRequestStopTimeoutFired = false;
app.AddTimeout (
TimeSpan.FromMilliseconds (10000),
() =>
{
output.WriteLine ("SAFETY: RequestStop Timeout fired - test took too long!");
requestStopTimeoutFired = true;
output.WriteLine ("SAFETY: RequestStop Timeout fired - test took too long - Assuming slow environment. Skipping assertions.");
safetyRequestStopTimeoutFired = true;
app.RequestStop ();
return false;
@@ -288,7 +289,7 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
);
var mainWindow = new Window { Title = "Main Window" };
var dialog = new Dialog { Title = "Dialog", Buttons = [new() { Text = "Ok" }] };
var dialog = new Dialog { Title = "Dialog", Buttons = [new () { Text = "Ok" }] };
var initialTimeoutCount = 0;
var timeoutCountDuringNestedRun = 0;
@@ -349,12 +350,13 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
// Assert
output.WriteLine ($"Final counts - Initial: {initialTimeoutCount}, During: {timeoutCountDuringNestedRun}, After: {timeoutCountAfterNestedRun}");
// The timeout queue should have pending timeouts throughout
Assert.True (initialTimeoutCount >= 0, "Should have timeouts in queue initially");
Assert.True (timeoutCountDuringNestedRun >= 0, "Should have timeouts in queue during nested run");
Assert.True (timeoutCountAfterNestedRun >= 0, "Should have timeouts in queue after nested run");
Assert.False (requestStopTimeoutFired, "Safety timeout should NOT have fired");
if (!safetyRequestStopTimeoutFired)
{
// The timeout queue should have pending timeouts throughout
Assert.True (initialTimeoutCount >= 0, "Should have timeouts in queue initially");
Assert.True (timeoutCountDuringNestedRun >= 0, "Should have timeouts in queue during nested run");
Assert.True (timeoutCountAfterNestedRun >= 0, "Should have timeouts in queue after nested run");
}
dialog.Dispose ();
mainWindow.Dispose ();
@@ -378,17 +380,17 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
var messageBoxClosed = false;
var mainWindow = new Window { Title = "Login Window" };
var messageBox = new Dialog { Title = "Success", Buttons = [new() { Text = "Ok" }] };
var messageBox = new Dialog { Title = "Success", Buttons = [new () { Text = "Ok" }] };
// Schedule a safety timeout that will ensure the app quits if test hangs
var requestStopTimeoutFired = false;
var safetyRequestStopTimeoutFired = false;
app.AddTimeout (
TimeSpan.FromMilliseconds (10000),
() =>
{
output.WriteLine ("SAFETY: RequestStop Timeout fired - test took too long!");
requestStopTimeoutFired = true;
output.WriteLine ("SAFETY: RequestStop Timeout fired - test took too long - Assuming slow environment. Skipping assertions.");
safetyRequestStopTimeoutFired = true;
app.RequestStop ();
return false;
@@ -448,13 +450,14 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
// Act
app.Run (mainWindow);
// Assert
Assert.True (enterFired, "Enter timeout should have fired");
Assert.True (messageBoxShown, "MessageBox should have been shown");
Assert.True (escFired, "ESC timeout should have fired during MessageBox"); // THIS WAS THE BUG - NOW FIXED!
Assert.True (messageBoxClosed, "MessageBox should have been closed");
Assert.False (requestStopTimeoutFired, "Safety timeout should NOT have fired");
if (!safetyRequestStopTimeoutFired)
{
// Assert
Assert.True (enterFired, "Enter timeout should have fired");
Assert.True (messageBoxShown, "MessageBox should have been shown");
Assert.True (escFired, "ESC timeout should have fired during MessageBox"); // THIS WAS THE BUG - NOW FIXED!
Assert.True (messageBoxClosed, "MessageBox should have been closed");
}
messageBox.Dispose ();
mainWindow.Dispose ();

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

@@ -3,14 +3,10 @@ using System.Text;
using UnitTests;
using Xunit.Abstractions;
// Alias Console to MockConsole so we don't accidentally use Console
namespace DriverTests;
public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
{
private readonly ITestOutputHelper _output = output;
[Fact]
public void AddRune ()
{
@@ -179,4 +175,36 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
driver.Dispose ();
}
[Fact]
public void AddStr_Glyph_On_Second_Cell_Of_Wide_Glyph_Outputs_Correctly ()
{
IDriver? driver = CreateFakeDriver ();
driver.SetScreenSize (6, 3);
driver!.Clip = new (driver.Screen);
driver.Move (1, 0);
driver.AddStr ("┌");
driver.Move (2, 0);
driver.AddStr ("─");
driver.Move (3, 0);
driver.AddStr ("┐");
driver.Clip.Exclude (new Region (new (1, 0, 3, 1)));
driver.Move (0, 0);
driver.AddStr ("🍎🍎🍎🍎");
DriverAssert.AssertDriverContentsAre (
"""
<EFBFBD>🍎
""",
output,
driver);
driver.Refresh ();
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;0;0;0m\x1b[48;2;0;0;0m<30>┌─┐🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m",
output, driver);
}
}

View File

@@ -1,4 +1,5 @@
#nullable enable
using System.Text;
using UnitTests;
using Xunit.Abstractions;

View File

@@ -92,6 +92,51 @@ public class DriverTests (ITestOutputHelper output) : FakeDriverBase
app.Dispose ();
}
// Tests fix for https://github.com/gui-cs/Terminal.Gui/issues/4258
[Theory]
[InlineData ("fake")]
[InlineData ("windows")]
[InlineData ("dotnet")]
[InlineData ("unix")]
public void All_Drivers_When_Clipped_AddStr_Glyph_On_Second_Cell_Of_Wide_Glyph_Outputs_Correctly (string driverName)
{
IApplication? app = Application.Create ();
app.Init (driverName);
IDriver driver = app.Driver!;
// Need to force "windows" driver to override legacy console mode for this test
driver.IsLegacyConsole = false;
driver.Force16Colors = false;
driver.SetScreenSize (6, 3);
driver!.Clip = new (driver.Screen);
driver.Move (1, 0);
driver.AddStr ("┌");
driver.Move (2, 0);
driver.AddStr ("─");
driver.Move (3, 0);
driver.AddStr ("┐");
driver.Clip.Exclude (new Region (new (1, 0, 3, 1)));
driver.Move (0, 0);
driver.AddStr ("🍎🍎🍎🍎");
DriverAssert.AssertDriverContentsAre (
"""
<EFBFBD>🍎
""",
output,
driver);
driver.Refresh ();
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;0;0;0m\x1b[48;2;0;0;0m<30>┌─┐🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m",
output, driver);
}
}
public class TestTop : Runnable

View File

@@ -1,6 +1,4 @@
#nullable enable
namespace DriverTests;
namespace DriverTests;
public class OutputBaseTests
{
@@ -9,7 +7,7 @@ public class OutputBaseTests
{
// Arrange
var output = new FakeOutput ();
IOutputBuffer buffer = output.LastBuffer!;
IOutputBuffer buffer = output.GetLastBuffer ()!;
buffer.SetSize (1, 1);
// Act
@@ -32,21 +30,21 @@ public class OutputBaseTests
// Create DriverImpl and associate it with the FakeOutput to test Sixel output
IDriver driver = new DriverImpl (
new FakeInputProcessor (null!),
new OutputBufferImpl (),
output,
new (new AnsiResponseParser ()),
new SizeMonitorImpl (output));
new FakeInputProcessor (null!),
new OutputBufferImpl (),
output,
new (new AnsiResponseParser ()),
new SizeMonitorImpl (output));
driver.Force16Colors = force16Colors;
IOutputBuffer buffer = output.LastBuffer!;
IOutputBuffer buffer = output.GetLastBuffer ()!;
buffer.SetSize (1, 1);
// Use a known RGB color and attribute
var fg = new Color (1, 2, 3);
var bg = new Color (4, 5, 6);
buffer.CurrentAttribute = new Attribute (fg, bg);
buffer.CurrentAttribute = new (fg, bg);
buffer.AddStr ("X");
// Act
@@ -59,7 +57,7 @@ public class OutputBaseTests
}
else if (!isLegacyConsole && force16Colors)
{
var expected16 = EscSeqUtils.CSI_SetForegroundColor (fg.GetAnsiColorCode ());
string expected16 = EscSeqUtils.CSI_SetForegroundColor (fg.GetAnsiColorCode ());
Assert.Contains (expected16, ansi);
}
else
@@ -78,7 +76,7 @@ public class OutputBaseTests
{
// Arrange
var output = new FakeOutput ();
IOutputBuffer buffer = output.LastBuffer!;
IOutputBuffer buffer = output.GetLastBuffer ()!;
buffer.SetSize (2, 1);
// Mark two characters as dirty by writing them into the buffer
@@ -92,7 +90,7 @@ public class OutputBaseTests
output.Write (buffer); // calls OutputBase.Write via FakeOutput
// Assert: content was written to the fake output and dirty flags cleared
Assert.Contains ("AB", output.Output);
Assert.Contains ("AB", output.GetLastOutput ());
Assert.False (buffer.Contents! [0, 0].IsDirty);
Assert.False (buffer.Contents! [0, 1].IsDirty);
}
@@ -105,7 +103,7 @@ public class OutputBaseTests
// Arrange
// FakeOutput exposes this because it's in test scope
var output = new FakeOutput { IsLegacyConsole = isLegacyConsole };
IOutputBuffer buffer = output.LastBuffer!;
IOutputBuffer buffer = output.GetLastBuffer ()!;
buffer.SetSize (3, 1);
// Write 'A' at col 0 and 'C' at col 2; leave col 1 untouched (not dirty)
@@ -122,15 +120,15 @@ public class OutputBaseTests
output.Write (buffer);
// Assert: both characters were written (use Contains to avoid CI side effects)
Assert.Contains ("A", output.Output);
Assert.Contains ("C", output.Output);
Assert.Contains ("A", output.GetLastOutput ());
Assert.Contains ("C", output.GetLastOutput ());
// Dirty flags cleared for the written cells
Assert.False (buffer.Contents! [0, 0].IsDirty);
Assert.False (buffer.Contents! [0, 2].IsDirty);
// Verify SetCursorPositionImpl was invoked by WriteToConsole (cursor set to a written column)
Assert.Equal (new Point (0, 0), output.GetCursorPosition ());
Assert.Equal (new (0, 0), output.GetCursorPosition ());
// Now write 'X' at col 0 to verify subsequent writes also work
buffer.Move (0, 0);
@@ -143,15 +141,84 @@ public class OutputBaseTests
output.Write (buffer);
// Assert: both characters were written (use Contains to avoid CI side effects)
Assert.Contains ("A", output.Output);
Assert.Contains ("C", output.Output);
Assert.Contains ("A", output.GetLastOutput ());
Assert.Contains ("C", output.GetLastOutput ());
// Dirty flags cleared for the written cells
Assert.False (buffer.Contents! [0, 0].IsDirty);
Assert.False (buffer.Contents! [0, 2].IsDirty);
// Verify SetCursorPositionImpl was invoked by WriteToConsole (cursor set to a written column)
Assert.Equal (new Point (2, 0), output.GetCursorPosition ());
Assert.Equal (new (2, 0), output.GetCursorPosition ());
}
[Theory]
[InlineData (true)]
[InlineData (false)]
public void Write_Virtual_Or_NonVirtual_Uses_WriteToConsole_And_Clears_Dirty_Flags_Mixed_Graphemes (bool isLegacyConsole)
{
// Arrange
// FakeOutput exposes this because it's in test scope
var output = new FakeOutput { IsLegacyConsole = isLegacyConsole };
IOutputBuffer buffer = output.GetLastBuffer ()!;
buffer.SetSize (3, 1);
// Write '🦮' at col 0 and 'A' at col 2
buffer.Move (0, 0);
buffer.AddStr ("🦮A");
// After the fix for https://github.com/gui-cs/Terminal.Gui/issues/4258:
// Writing a wide glyph at column 0 no longer sets column 1 to IsDirty = false.
// Column 1 retains whatever state it had (in this case, it was initialized as dirty
// by ClearContents, but may have been cleared by a previous Write call).
//
// What we care about is that wide glyphs work correctly and don't prevent
// other content from being drawn at odd columns.
Assert.True (buffer.Contents! [0, 0].IsDirty);
// Column 1 state depends on whether it was cleared by a previous Write - don't assert
Assert.True (buffer.Contents! [0, 2].IsDirty);
// Act
output.Write (buffer);
Assert.Contains ("🦮", output.GetLastOutput ());
Assert.Contains ("A", output.GetLastOutput ());
// Dirty flags cleared for the written cells
// Column 0 was written (wide glyph)
Assert.False (buffer.Contents! [0, 0].IsDirty);
// Column 1 was skipped by OutputBase.Write because column 0 had a wide glyph
// So its dirty flag remains true (it was initialized as dirty by ClearContents)
Assert.True (buffer.Contents! [0, 1].IsDirty);
// Column 2 was written ('A')
Assert.False (buffer.Contents! [0, 2].IsDirty);
Assert.Equal (new (0, 0), output.GetCursorPosition ());
// Now write 'X' at col 1 which invalidates the wide glyph at col 0
buffer.Move (1, 0);
buffer.AddStr ("X");
// Confirm dirtiness state before to write
Assert.True (buffer.Contents! [0, 0].IsDirty); // Invalidated by writing at col 1
Assert.True (buffer.Contents! [0, 1].IsDirty); // Just written
Assert.True (buffer.Contents! [0, 2].IsDirty); // Marked dirty by writing at col 1
output.Write (buffer);
Assert.Contains ("<22>", output.GetLastOutput ());
Assert.Contains ("X", output.GetLastOutput ());
// Dirty flags cleared for the written cells
Assert.False (buffer.Contents! [0, 0].IsDirty);
Assert.False (buffer.Contents! [0, 1].IsDirty);
Assert.False (buffer.Contents! [0, 2].IsDirty);
// Verify SetCursorPositionImpl was invoked by WriteToConsole (cursor set to a written column)
Assert.Equal (new (0, 0), output.GetCursorPosition ());
}
[Theory]
@@ -161,7 +228,7 @@ public class OutputBaseTests
{
// Arrange
var output = new FakeOutput ();
IOutputBuffer buffer = output.LastBuffer!;
IOutputBuffer buffer = output.GetLastBuffer ()!;
buffer.SetSize (1, 1);
// Ensure the buffer has some content so Write traverses rows
@@ -171,16 +238,16 @@ public class OutputBaseTests
var s = new SixelToRender
{
SixelData = "SIXEL-DATA",
ScreenPosition = new Point (4, 2)
ScreenPosition = new (4, 2)
};
// Create DriverImpl and associate it with the FakeOutput to test Sixel output
IDriver driver = new DriverImpl (
new FakeInputProcessor (null!),
new OutputBufferImpl (),
output,
new (new AnsiResponseParser ()),
new SizeMonitorImpl (output));
new FakeInputProcessor (null!),
new OutputBufferImpl (),
output,
new (new AnsiResponseParser ()),
new SizeMonitorImpl (output));
// Add the Sixel to the driver
driver.GetSixels ().Enqueue (s);
@@ -194,7 +261,7 @@ public class OutputBaseTests
if (!isLegacyConsole)
{
// Assert: Sixel data was emitted (use Contains to avoid equality/side-effects)
Assert.Contains ("SIXEL-DATA", output.Output);
Assert.Contains ("SIXEL-DATA", output.GetLastOutput ());
// Cursor was moved to Sixel position
Assert.Equal (s.ScreenPosition, output.GetCursorPosition ());
@@ -202,7 +269,7 @@ public class OutputBaseTests
else
{
// Assert: Sixel data was NOT emitted
Assert.DoesNotContain ("SIXEL-DATA", output.Output);
Assert.DoesNotContain ("SIXEL-DATA", output.GetLastOutput ());
// Cursor was NOT moved to Sixel position
Assert.NotEqual (s.ScreenPosition, output.GetCursorPosition ());
@@ -215,4 +282,4 @@ public class OutputBaseTests
app.Dispose ();
}
}
}

View File

@@ -240,7 +240,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase
Point screenPos = new Point (15, 15);
view.LineCanvas.AddLine (screenPos, 5, Orientation.Horizontal, LineStyle.Single);
view.RenderLineCanvas ();
view.RenderLineCanvas (null);
// Verify the line was drawn (check for horizontal line character)
for (int i = 0; i < 5; i++)
@@ -272,7 +272,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase
Assert.NotEqual (Rectangle.Empty, view.LineCanvas.Bounds);
view.RenderLineCanvas ();
view.RenderLineCanvas (null);
// LineCanvas should be cleared after rendering
Assert.Equal (Rectangle.Empty, view.LineCanvas.Bounds);
@@ -302,7 +302,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase
Rectangle boundsBefore = view.LineCanvas.Bounds;
view.RenderLineCanvas ();
view.RenderLineCanvas (null);
// LineCanvas should NOT be cleared when SuperViewRendersLineCanvas is true
Assert.Equal (boundsBefore, view.LineCanvas.Bounds);

View File

@@ -1,19 +1,18 @@
#nullable enable
using System.Text;
using UnitTests;
using Xunit.Abstractions;
namespace ViewBaseTests.Drawing;
public class ViewDrawingClippingTests () : FakeDriverBase
public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBase
{
#region GetClip / SetClip Tests
[Fact]
public void GetClip_ReturnsDriverClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
var region = new Region (new Rectangle (10, 10, 20, 20));
IDriver driver = CreateFakeDriver ();
var region = new Region (new (10, 10, 20, 20));
driver.Clip = region;
View view = new () { Driver = driver };
@@ -26,8 +25,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void SetClip_NullRegion_DoesNothing ()
{
IDriver driver = CreateFakeDriver (80, 25);
var original = new Region (new Rectangle (5, 5, 10, 10));
IDriver driver = CreateFakeDriver ();
var original = new Region (new (5, 5, 10, 10));
driver.Clip = original;
View view = new () { Driver = driver };
@@ -40,8 +39,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void SetClip_ValidRegion_SetsDriverClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
var region = new Region (new Rectangle (10, 10, 30, 30));
IDriver driver = CreateFakeDriver ();
var region = new Region (new (10, 10, 30, 30));
View view = new () { Driver = driver };
view.SetClip (region);
@@ -56,8 +55,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void SetClipToScreen_ReturnsPreviousClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
var original = new Region (new Rectangle (5, 5, 10, 10));
IDriver driver = CreateFakeDriver ();
var original = new Region (new (5, 5, 10, 10));
driver.Clip = original;
View view = new () { Driver = driver };
@@ -70,7 +69,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void SetClipToScreen_SetsClipToScreen ()
{
IDriver driver = CreateFakeDriver (80, 25);
IDriver driver = CreateFakeDriver ();
View view = new () { Driver = driver };
view.SetClipToScreen ();
@@ -87,15 +86,15 @@ public class ViewDrawingClippingTests () : FakeDriverBase
public void ExcludeFromClip_Rectangle_NullDriver_DoesNotThrow ()
{
View view = new () { Driver = null };
var exception = Record.Exception (() => view.ExcludeFromClip (new Rectangle (5, 5, 10, 10)));
Exception? exception = Record.Exception (() => view.ExcludeFromClip (new Rectangle (5, 5, 10, 10)));
Assert.Null (exception);
}
[Fact]
public void ExcludeFromClip_Rectangle_ExcludesArea ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (new Rectangle (0, 0, 80, 25));
IDriver driver = CreateFakeDriver ();
driver.Clip = new (new (0, 0, 80, 25));
View view = new () { Driver = driver };
var toExclude = new Rectangle (10, 10, 20, 20);
@@ -111,19 +110,18 @@ public class ViewDrawingClippingTests () : FakeDriverBase
{
View view = new () { Driver = null };
var exception = Record.Exception (() => view.ExcludeFromClip (new Region (new Rectangle (5, 5, 10, 10))));
Exception? exception = Record.Exception (() => view.ExcludeFromClip (new Region (new (5, 5, 10, 10))));
Assert.Null (exception);
}
[Fact]
public void ExcludeFromClip_Region_ExcludesArea ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (new Rectangle (0, 0, 80, 25));
IDriver driver = CreateFakeDriver ();
driver.Clip = new (new (0, 0, 80, 25));
View view = new () { Driver = driver };
var toExclude = new Region (new Rectangle (10, 10, 20, 20));
var toExclude = new Region (new (10, 10, 20, 20));
view.ExcludeFromClip (toExclude);
// Verify the region was excluded
@@ -150,8 +148,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void AddFrameToClip_IntersectsWithFrame ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -171,7 +169,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Assert.NotNull (driver.Clip);
// The clip should now be the intersection of the screen and the view's frame
Rectangle expectedBounds = new Rectangle (1, 1, 20, 20);
var expectedBounds = new Rectangle (1, 1, 20, 20);
Assert.Equal (expectedBounds, driver.Clip.GetBounds ());
}
@@ -194,8 +192,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void AddViewportToClip_IntersectsWithViewport ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -222,8 +220,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void AddViewportToClip_WithClipContentOnly_LimitsToVisibleContent ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -260,7 +258,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
public void ClipRegions_StackCorrectly_WithNestedViews ()
{
IDriver driver = CreateFakeDriver (100, 100);
driver.Clip = new Region (driver.Screen);
driver.Clip = new (driver.Screen);
var superView = new View
{
@@ -278,7 +276,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
X = 5,
Y = 5,
Width = 30,
Height = 30,
Height = 30
};
superView.Add (view);
superView.LayoutSubViews ();
@@ -296,14 +294,15 @@ public class ViewDrawingClippingTests () : FakeDriverBase
// Restore superView clip
view.SetClip (superViewClip);
// Assert.Equal (superViewBounds, driver.Clip.GetBounds ());
}
[Fact]
public void ClipRegions_RespectPreviousClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
var initialClip = new Region (new Rectangle (20, 20, 40, 40));
IDriver driver = CreateFakeDriver ();
var initialClip = new Region (new (20, 20, 40, 40));
driver.Clip = initialClip;
var view = new View
@@ -322,9 +321,9 @@ public class ViewDrawingClippingTests () : FakeDriverBase
// The new clip should be the intersection of the initial clip and the view's frame
Rectangle expected = Rectangle.Intersect (
initialClip.GetBounds (),
view.FrameToScreen ()
);
initialClip.GetBounds (),
view.FrameToScreen ()
);
Assert.Equal (expected, driver.Clip.GetBounds ());
@@ -340,8 +339,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void AddFrameToClip_EmptyFrame_WorksCorrectly ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -364,18 +363,18 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void AddViewportToClip_EmptyViewport_WorksCorrectly ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
X = 1,
Y = 1,
Width = 1, // Minimal size to have adornments
Width = 1, // Minimal size to have adornments
Height = 1,
Driver = driver
};
view.Border!.Thickness = new Thickness (1);
view.Border!.Thickness = new (1);
view.BeginInit ();
view.EndInit ();
view.LayoutSubViews ();
@@ -391,12 +390,12 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void ClipRegions_OutOfBounds_HandledCorrectly ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
X = 100, // Outside screen bounds
X = 100, // Outside screen bounds
Y = 100,
Width = 20,
Height = 20,
@@ -409,6 +408,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Region? previous = view.AddFrameToClip ();
Assert.NotNull (previous);
// The clip should be empty since the view is outside the screen
Assert.True (driver.Clip.IsEmpty () || !driver.Clip.Contains (100, 100));
}
@@ -420,8 +420,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void Clip_Set_BeforeDraw_ClipsDrawing ()
{
IDriver driver = CreateFakeDriver (80, 25);
var clip = new Region (new Rectangle (10, 10, 10, 10));
IDriver driver = CreateFakeDriver ();
var clip = new Region (new (10, 10, 10, 10));
driver.Clip = clip;
var view = new View
@@ -445,8 +445,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void Draw_UpdatesDriverClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -464,14 +464,15 @@ public class ViewDrawingClippingTests () : FakeDriverBase
// Clip should be updated to exclude the drawn view
Assert.NotNull (driver.Clip);
// Assert.False (driver.Clip.Contains (15, 15)); // Point inside the view should be excluded
}
[Fact]
public void Draw_WithSubViews_ClipsCorrectly ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var superView = new View
{
@@ -491,13 +492,277 @@ public class ViewDrawingClippingTests () : FakeDriverBase
// Both superView and view should be excluded from clip
Assert.NotNull (driver.Clip);
// Assert.False (driver.Clip.Contains (15, 15)); // Point in superView should be excluded
}
/// <summary>
/// Tests that wide glyphs (🍎) are correctly clipped when overlapped by bordered subviews
/// at different column alignments (even vs odd). Demonstrates:
/// 1. Full clipping at even columns (X=0, X=2)
/// 2. Partial clipping at odd columns (X=1) resulting in half-glyphs (<28>)
/// 3. The recursive draw flow and clip exclusion mechanism
///
/// For detailed draw flow documentation, see ViewDrawingClippingTests.DrawFlow.md
/// </summary>
[Fact]
public void Draw_WithBorderSubView_DrawsCorrectly ()
{
IApplication app = Application.Create ();
app.Init ("fake");
IDriver driver = app!.Driver!;
driver.SetScreenSize (30, 20);
driver!.Clip = new (driver.Screen);
var superView = new Runnable ()
{
X = 0,
Y = 0,
Width = Dim.Auto () + 4,
Height = Dim.Auto () + 1,
Driver = driver
};
Rune codepoint = Glyphs.Apple;
superView.DrawingContent += (s, e) =>
{
var view = s as View;
for (var r = 0; r < view!.Viewport.Height; r++)
{
for (var c = 0; c < view.Viewport.Width; c += 2)
{
if (codepoint != default (Rune))
{
view.AddRune (c, r, codepoint);
}
}
}
e.DrawContext?.AddDrawnRectangle (view.Viewport);
e.Cancel = true;
};
var viewWithBorderAtX0 = new View
{
Text = "viewWithBorderAtX0",
BorderStyle = LineStyle.Dashed,
X = 0,
Y = 1,
Width = Dim.Auto (),
Height = 3
};
var viewWithBorderAtX1 = new View
{
Text = "viewWithBorderAtX1",
BorderStyle = LineStyle.Dashed,
X = 1,
Y = Pos.Bottom (viewWithBorderAtX0) + 1,
Width = Dim.Auto (),
Height = 3
};
var viewWithBorderAtX2 = new View
{
Text = "viewWithBorderAtX2",
BorderStyle = LineStyle.Dashed,
X = 2,
Y = Pos.Bottom (viewWithBorderAtX1) + 1,
Width = Dim.Auto (),
Height = 3
};
superView.Add (viewWithBorderAtX0, viewWithBorderAtX1, viewWithBorderAtX2);
app.Begin (superView);
// Begin calls LayoutAndDraw, so no need to call it again here
// app.LayoutAndDraw();
DriverAssert.AssertDriverContentsAre (
"""
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
🍎🍎🍎
viewWithBorderAtX0🍎🍎🍎
🍎🍎🍎
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
<EFBFBD> 🍎🍎
<EFBFBD>viewWithBorderAtX1 🍎🍎
<EFBFBD> 🍎🍎
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
🍎🍎🍎
🍎viewWithBorderAtX2🍎🍎
🍎🍎🍎
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
""",
output,
driver);
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m┆viewWithBorderAtX0┆🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m<39>┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m<39>┆viewWithBorderAtX1┆ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m<39>└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎┆viewWithBorderAtX2┆🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m",
output, driver);
DriverImpl? driverImpl = driver as DriverImpl;
FakeOutput? fakeOutput = driverImpl!.GetOutput () as FakeOutput;
output.WriteLine ("Driver Output After Redraw:\n" + driver.GetOutput().GetLastOutput());
// BUGBUG: Border.set_LineStyle does not call SetNeedsDraw
viewWithBorderAtX1!.Border!.LineStyle = LineStyle.Single;
viewWithBorderAtX1.Border!.SetNeedsDraw ();
app.LayoutAndDraw ();
DriverAssert.AssertDriverContentsAre (
"""
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
🍎🍎🍎
viewWithBorderAtX0🍎🍎🍎
🍎🍎🍎
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
<EFBFBD> 🍎🍎
<EFBFBD>viewWithBorderAtX1 🍎🍎
<EFBFBD> 🍎🍎
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
🍎🍎🍎
🍎viewWithBorderAtX2🍎🍎
🍎🍎🍎
🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎
""",
output,
driver);
}
[Fact]
public void Draw_WithBorderSubView_At_Col1_In_WideGlyph_DrawsCorrectly ()
{
IApplication app = Application.Create ();
app.Init ("fake");
IDriver driver = app!.Driver!;
driver.SetScreenSize (6, 3); // Minimal: 6 cols wide (3 for content + 2 for border + 1), 3 rows high (1 for content + 2 for border)
driver!.Clip = new (driver.Screen);
var superView = new Runnable ()
{
X = 0,
Y = 0,
Width = Dim.Fill (),
Height = Dim.Fill (),
Driver = driver
};
Rune codepoint = Glyphs.Apple;
superView.DrawingContent += (s, e) =>
{
View? view = s as View;
view?.AddStr (0, 0, "🍎🍎🍎🍎");
view?.AddStr (0, 1, "🍎🍎🍎🍎");
view?.AddStr (0, 2, "🍎🍎🍎🍎");
e.DrawContext?.AddDrawnRectangle (view!.Viewport);
e.Cancel = true;
};
// Minimal border at X=1 (odd column), Width=3, Height=3 (includes border)
var viewWithBorder = new View
{
Text = "X",
BorderStyle = LineStyle.Single,
X = 1,
Y = 0,
Width = 3,
Height = 3
};
superView.Add (viewWithBorder);
app.Begin (superView);
DriverAssert.AssertDriverContentsAre (
"""
<EFBFBD>🍎
<EFBFBD>X🍎
<EFBFBD>🍎
""",
output,
driver);
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m<39>┌─┐🍎<E29490>│X│🍎<E29482>└─┘🍎",
output, driver);
DriverImpl? driverImpl = driver as DriverImpl;
FakeOutput? fakeOutput = driverImpl!.GetOutput () as FakeOutput;
output.WriteLine ("Driver Output:\n" + fakeOutput!.GetLastOutput ());
}
[Fact]
public void Draw_WithBorderSubView_At_Col3_In_WideGlyph_DrawsCorrectly ()
{
IApplication app = Application.Create ();
app.Init ("fake");
IDriver driver = app!.Driver!;
driver.SetScreenSize (6, 3); // Screen: 6 cols wide, 3 rows high; enough for 3x3 border subview at col 3 plus content on the left
driver!.Clip = new (driver.Screen);
var superView = new Runnable ()
{
X = 0,
Y = 0,
Width = Dim.Fill (),
Height = Dim.Fill (),
Driver = driver
};
Rune codepoint = Glyphs.Apple;
superView.DrawingContent += (s, e) =>
{
View? view = s as View;
view?.AddStr (0, 0, "🍎🍎🍎🍎");
view?.AddStr (0, 1, "🍎🍎🍎🍎");
view?.AddStr (0, 2, "🍎🍎🍎🍎");
e.DrawContext?.AddDrawnRectangle (view!.Viewport);
e.Cancel = true;
};
// Minimal border at X=3 (odd column), Width=3, Height=3 (includes border)
var viewWithBorder = new View
{
Text = "X",
BorderStyle = LineStyle.Single,
X = 3,
Y = 0,
Width = 3,
Height = 3
};
superView.Add (viewWithBorder);
app.Begin (superView);
DriverAssert.AssertDriverContentsAre (
"""
🍎<EFBFBD>
🍎<EFBFBD>X
🍎<EFBFBD>
""",
output,
driver);
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎<6D>┌─┐🍎<E29490>│X│🍎<E29482>└─┘",
output, driver);
DriverImpl? driverImpl = driver as DriverImpl;
FakeOutput? fakeOutput = driverImpl!.GetOutput () as FakeOutput;
output.WriteLine ("Driver Output:\n" + fakeOutput!.GetLastOutput ());
}
[Fact]
public void Draw_NonVisibleView_DoesNotUpdateClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
IDriver driver = CreateFakeDriver ();
var originalClip = new Region (driver.Screen);
driver.Clip = originalClip.Clone ();
@@ -522,8 +787,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void ExcludeFromClip_ExcludesRegion ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -542,13 +807,12 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Assert.NotNull (driver.Clip);
Assert.False (driver.Clip.Contains (20, 20)); // Point inside excluded rect should not be in clip
}
[Fact]
public void ExcludeFromClip_WithNullClip_DoesNotThrow ()
{
IDriver driver = CreateFakeDriver (80, 25);
IDriver driver = CreateFakeDriver ();
driver.Clip = null!;
var view = new View
@@ -560,10 +824,9 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Driver = driver
};
var exception = Record.Exception (() => view.ExcludeFromClip (new Rectangle (15, 15, 10, 10)));
Exception? exception = Record.Exception (() => view.ExcludeFromClip (new Rectangle (15, 15, 10, 10)));
Assert.Null (exception);
}
#endregion
@@ -573,7 +836,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void SetClip_SetsDriverClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
IDriver driver = CreateFakeDriver ();
var view = new View
{
@@ -584,7 +847,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Driver = driver
};
var newClip = new Region (new Rectangle (5, 5, 30, 30));
var newClip = new Region (new (5, 5, 30, 30));
view.SetClip (newClip);
Assert.Equal (newClip, driver.Clip);
@@ -593,8 +856,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact (Skip = "See BUGBUG in SetClip")]
public void SetClip_WithNullClip_ClearsClip ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (new Rectangle (10, 10, 20, 20));
IDriver driver = CreateFakeDriver ();
driver.Clip = new (new (10, 10, 20, 20));
var view = new View
{
@@ -613,7 +876,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void Draw_Excludes_View_From_Clip ()
{
IDriver driver = CreateFakeDriver (80, 25);
IDriver driver = CreateFakeDriver ();
var originalClip = new Region (driver.Screen);
driver.Clip = originalClip.Clone ();
@@ -641,8 +904,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void Draw_EmptyViewport_DoesNotCrash ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -652,13 +915,13 @@ public class ViewDrawingClippingTests () : FakeDriverBase
Height = 1,
Driver = driver
};
view.Border!.Thickness = new Thickness (1);
view.Border!.Thickness = new (1);
view.BeginInit ();
view.EndInit ();
view.LayoutSubViews ();
// With border of 1, viewport should be empty (0x0 or negative)
var exception = Record.Exception (() => view.Draw ());
Exception? exception = Record.Exception (() => view.Draw ());
Assert.Null (exception);
}
@@ -666,8 +929,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void Draw_VeryLargeView_HandlesClippingCorrectly ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -681,7 +944,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
view.EndInit ();
view.LayoutSubViews ();
var exception = Record.Exception (() => view.Draw ());
Exception? exception = Record.Exception (() => view.Draw ());
Assert.Null (exception);
}
@@ -689,8 +952,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void Draw_NegativeCoordinates_HandlesClippingCorrectly ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -704,7 +967,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
view.EndInit ();
view.LayoutSubViews ();
var exception = Record.Exception (() => view.Draw ());
Exception? exception = Record.Exception (() => view.Draw ());
Assert.Null (exception);
}
@@ -712,8 +975,8 @@ public class ViewDrawingClippingTests () : FakeDriverBase
[Fact]
public void Draw_OutOfScreenBounds_HandlesClippingCorrectly ()
{
IDriver driver = CreateFakeDriver (80, 25);
driver.Clip = new Region (driver.Screen);
IDriver driver = CreateFakeDriver ();
driver.Clip = new (driver.Screen);
var view = new View
{
@@ -727,7 +990,7 @@ public class ViewDrawingClippingTests () : FakeDriverBase
view.EndInit ();
view.LayoutSubViews ();
var exception = Record.Exception (() => view.Draw ());
Exception? exception = Record.Exception (() => view.Draw ());
Assert.Null (exception);
}

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.
@@ -32,9 +32,9 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
};
Point? receivedPosition = null;
var eventReceived = false;
bool eventReceived = false;
view.MouseEvent += (sender, args) =>
view.MouseEvent += (_, args) =>
{
eventReceived = true;
receivedPosition = args.Position;
@@ -90,9 +90,9 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
};
Point? receivedPosition = null;
var eventReceived = false;
bool eventReceived = false;
view.MouseEvent += (sender, args) =>
view.MouseEvent += (_, args) =>
{
eventReceived = true;
receivedPosition = args.Position;
@@ -100,7 +100,7 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
MouseEventArgs mouseEvent = new ()
{
Position = new Point (viewRelativeX, viewRelativeY),
Position = new (viewRelativeX, viewRelativeY),
Flags = MouseFlags.Button1Clicked
};
@@ -146,9 +146,9 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
superView.Add (subView);
Point? subViewReceivedPosition = null;
var subViewEventReceived = false;
bool subViewEventReceived = false;
subView.MouseEvent += (sender, args) =>
subView.MouseEvent += (_, args) =>
{
subViewEventReceived = true;
subViewReceivedPosition = args.Position;
@@ -175,7 +175,7 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
}
[Fact]
public void MouseClick_OnSubView_RaisesMouseClickEvent ()
public void MouseClick_OnSubView_RaisesSelectingEvent ()
{
// Arrange
View superView = new ()
@@ -194,8 +194,8 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
superView.Add (subView);
var clickCount = 0;
subView.MouseClick += (sender, args) => clickCount++;
int selectingCount = 0;
subView.Selecting += (_, _) => selectingCount++;
MouseEventArgs mouseEvent = new ()
{
@@ -207,7 +207,7 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
subView.NewMouseEvent (mouseEvent);
// Assert
Assert.Equal (1, clickCount);
Assert.Equal (1, selectingCount);
subView.Dispose ();
superView.Dispose ();
@@ -222,20 +222,20 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
{
// Arrange
View view = new () { Width = 10, Height = 10 };
var handlerCalled = false;
var clickHandlerCalled = false;
bool handlerCalled = false;
bool clickHandlerCalled = false;
view.MouseEvent += (sender, args) =>
view.MouseEvent += (_, args) =>
{
handlerCalled = true;
args.Handled = true; // Mark as handled
};
view.MouseClick += (sender, args) => { clickHandlerCalled = true; };
view.MouseEvent += (_, e) => { clickHandlerCalled = !e.IsSingleDoubleOrTripleClicked; ; };
MouseEventArgs mouseEvent = new ()
{
Position = new Point (5, 5),
Position = new (5, 5),
Flags = MouseFlags.Button1Clicked
};
@@ -255,20 +255,17 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
{
// Arrange
View view = new () { Width = 10, Height = 10 };
var eventHandlerCalled = false;
var clickHandlerCalled = false;
bool eventHandlerCalled = false;
view.MouseEvent += (sender, args) =>
view.MouseEvent += (_, _) =>
{
eventHandlerCalled = true;
// Don't set Handled = true
};
view.MouseClick += (sender, args) => { clickHandlerCalled = true; };
MouseEventArgs mouseEvent = new ()
{
Position = new Point (5, 5),
Position = new (5, 5),
Flags = MouseFlags.Button1Clicked
};
@@ -277,7 +274,6 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
// Assert
Assert.True (eventHandlerCalled);
Assert.True (clickHandlerCalled); // Click handler should be called when event is not handled
view.Dispose ();
}
@@ -287,18 +283,17 @@ 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 };
var pressedCount = 0;
var releasedCount = 0;
var clickedCount = 0;
int pressedCount = 0;
int releasedCount = 0;
view.MouseEvent += (sender, args) =>
view.MouseEvent += (_, args) =>
{
if (args.Flags.HasFlag (MouseFlags.Button1Pressed))
{
@@ -311,11 +306,9 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
}
};
view.MouseClick += (sender, args) => { clickedCount++; };
MouseEventArgs mouseEvent = new ()
{
Position = new Point (5, 5),
Position = new (5, 5),
Flags = flags
};
@@ -325,7 +318,6 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
// Assert
Assert.Equal (expectedPressed, pressedCount);
Assert.Equal (expectedReleased, releasedCount);
Assert.Equal (expectedClicked, clickedCount);
view.Dispose ();
}
@@ -339,13 +331,13 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
{
// Arrange
View view = new () { Width = 10, Height = 10 };
var clickCount = 0;
int clickCount = 0;
view.MouseClick += (sender, args) => clickCount++;
view.MouseEvent += (_, a) => clickCount += a.IsSingleDoubleOrTripleClicked ? 1 : 0;
MouseEventArgs mouseEvent = new ()
{
Position = new Point (5, 5),
Position = new (5, 5),
Flags = clickFlag
};
@@ -373,12 +365,12 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
Enabled = false
};
var eventCalled = false;
view.MouseEvent += (sender, args) => { eventCalled = true; };
bool eventCalled = false;
view.MouseEvent += (_, _) => { eventCalled = true; };
MouseEventArgs mouseEvent = new ()
{
Position = new Point (5, 5),
Position = new (5, 5),
Flags = MouseFlags.Button1Clicked
};
@@ -392,7 +384,7 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
}
[Fact]
public void View_Disabled_DoesNotRaiseMouseClickEvent ()
public void View_Disabled_DoesNotRaiseSelectingEvent ()
{
// Arrange
View view = new ()
@@ -402,12 +394,12 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
Enabled = false
};
var clickCalled = false;
view.MouseClick += (sender, args) => { clickCalled = true; };
bool selectingCalled = false;
view.Selecting += (_, _) => { selectingCalled = true; };
MouseEventArgs mouseEvent = new ()
{
Position = new Point (5, 5),
Position = new (5, 5),
Flags = MouseFlags.Button1Clicked
};
@@ -415,7 +407,7 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
view.NewMouseEvent (mouseEvent);
// Assert
Assert.False (clickCalled);
Assert.False (selectingCalled);
view.Dispose ();
}
@@ -445,7 +437,7 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
MouseEventArgs mouseEvent = new ()
{
Position = new Point (2, 2),
Position = new (2, 2),
Flags = MouseFlags.Button1Clicked
};
@@ -475,12 +467,12 @@ public class MouseEventRoutingTests (ITestOutputHelper output)
superView.Add (view);
var selectingCount = 0;
view.Selecting += (sender, args) => selectingCount++;
int selectingCount = 0;
view.Selecting += (_, _) => selectingCount++;
MouseEventArgs mouseEvent = new ()
{
Position = new Point (5, 5),
Position = new (5, 5),
Flags = MouseFlags.Button1Clicked
};