mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-30 09:47:58 +01:00
* Fixed MouseGrabView bug. Added extensive test coverage for `Keyboard`, `Mouse`, `Timeout`, and `Popover` functionalities, including edge cases and concurrent access. Introduced parameterized and data-driven tests to reduce redundancy and improve clarity. Refactored codebase for modularity and maintainability, introducing new namespaces and reorganizing classes. Enhanced `MouseImpl`, `KeyboardImpl`, and `Runnable` implementations with improved event handling, thread safety, and support for the Terminal.Gui Cancellable Work Pattern (CWP). Removed deprecated code and legacy tests, such as `LogarithmicTimeout` and `SmoothAcceleratingTimeout`. Fixed bugs related to mouse grabbing during drag operations and unbalanced `ApplicationImpl.Begin/End` calls. Improved documentation and code readability with modern C# features. * Code cleanup. * Update Tests/UnitTestsParallelizable/Application/Runnable/RunnableIntegrationTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Improve null handling and simplify test setup In `MouseImpl.cs`, added an early `return` after the `UngrabMouse()` call within the `if (view is null)` block to prevent further execution when `view` is `null`, improving null reference handling. In `RunnableIntegrationTests.cs`, removed the initialization of the `IApplication` object (`app`) from the `MultipleRunnables_IndependentResults` test method, simplifying the test setup and focusing on runnable behavior. * Code cleanup * API doc link cleanup --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,520 @@
|
||||
// ReSharper disable AccessToDisposedClosure
|
||||
|
||||
#nullable enable
|
||||
namespace ApplicationTests.Keyboard;
|
||||
|
||||
/// <summary>
|
||||
/// Tests to verify that KeyboardImpl is thread-safe for concurrent access scenarios.
|
||||
/// </summary>
|
||||
public class KeyboardImplThreadSafetyTests
|
||||
{
|
||||
[Fact]
|
||||
public void AddCommand_ConcurrentAccess_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
List<Exception> exceptions = [];
|
||||
const int NUM_THREADS = 10;
|
||||
const int OPERATIONS_PER_THREAD = 50;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = [];
|
||||
|
||||
for (var i = 0; i < NUM_THREADS; i++)
|
||||
{
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
// AddKeyBindings internally calls AddCommand multiple times
|
||||
keyboard.AddKeyBindings ();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Expected - AddKeyBindings tries to add keys that already exist
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_WhileOperationsInProgress_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication? app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
var keyboard = new KeyboardImpl { App = app };
|
||||
keyboard.AddKeyBindings ();
|
||||
List<Exception> exceptions = [];
|
||||
var continueRunning = true;
|
||||
|
||||
// Act
|
||||
Task operationsTask = Task.Run (() =>
|
||||
{
|
||||
while (continueRunning)
|
||||
{
|
||||
try
|
||||
{
|
||||
keyboard.InvokeCommandsBoundToKey (Key.Q.WithCtrl);
|
||||
IEnumerable<KeyValuePair<Key, KeyBinding>> bindings = keyboard.KeyBindings.GetBindings ();
|
||||
int count = bindings.Count ();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Expected - keyboard was disposed
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Give operations a chance to start
|
||||
Thread.Sleep (10);
|
||||
|
||||
// Dispose while operations are running
|
||||
keyboard.Dispose ();
|
||||
continueRunning = false;
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
operationsTask.Wait (TimeSpan.FromSeconds (2));
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
app.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeCommand_ConcurrentAccess_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication? app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
var keyboard = new KeyboardImpl { App = app };
|
||||
keyboard.AddKeyBindings ();
|
||||
List<Exception> exceptions = [];
|
||||
const int NUM_THREADS = 10;
|
||||
const int OPERATIONS_PER_THREAD = 50;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = new ();
|
||||
|
||||
for (var i = 0; i < NUM_THREADS; i++)
|
||||
{
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var binding = new KeyBinding ([Command.Quit]);
|
||||
keyboard.InvokeCommand (Command.Quit, Key.Q.WithCtrl, binding);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
app.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeCommandsBoundToKey_ConcurrentAccess_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication? app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
var keyboard = new KeyboardImpl { App = app };
|
||||
keyboard.AddKeyBindings ();
|
||||
List<Exception> exceptions = [];
|
||||
const int NUM_THREADS = 10;
|
||||
const int OPERATIONS_PER_THREAD = 50;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = [];
|
||||
|
||||
for (var i = 0; i < NUM_THREADS; i++)
|
||||
{
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
keyboard.InvokeCommandsBoundToKey (Key.Q.WithCtrl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
app.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyBindings_ConcurrentAdd_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
|
||||
// Don't call AddKeyBindings here to avoid conflicts
|
||||
List<Exception> exceptions = [];
|
||||
const int NUM_THREADS = 10;
|
||||
const int OPERATIONS_PER_THREAD = 50;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = new ();
|
||||
|
||||
for (var i = 0; i < NUM_THREADS; i++)
|
||||
{
|
||||
int threadId = i;
|
||||
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use unique keys per thread to avoid conflicts
|
||||
Key key = Key.F1 + threadId * OPERATIONS_PER_THREAD + j;
|
||||
keyboard.KeyBindings.Add (key, Command.Refresh);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Expected - duplicate key
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Expected - invalid key
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyDown_KeyUp_Events_ConcurrentSubscription_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
keyboard.AddKeyBindings ();
|
||||
List<Exception> exceptions = [];
|
||||
const int NUM_THREADS = 10;
|
||||
const int OPERATIONS_PER_THREAD = 20;
|
||||
var keyDownCount = 0;
|
||||
var keyUpCount = 0;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = new ();
|
||||
|
||||
// Threads subscribing to events
|
||||
for (var i = 0; i < NUM_THREADS; i++)
|
||||
{
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
EventHandler<Key> handler = (s, e) => { Interlocked.Increment (ref keyDownCount); };
|
||||
keyboard.KeyDown += handler;
|
||||
keyboard.KeyDown -= handler;
|
||||
|
||||
EventHandler<Key> upHandler = (s, e) => { Interlocked.Increment (ref keyUpCount); };
|
||||
keyboard.KeyUp += upHandler;
|
||||
keyboard.KeyUp -= upHandler;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyProperty_Setters_ConcurrentAccess_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
|
||||
// Initialize once before concurrent access
|
||||
keyboard.AddKeyBindings ();
|
||||
List<Exception> exceptions = [];
|
||||
const int NUM_THREADS = 10;
|
||||
const int OPERATIONS_PER_THREAD = 20;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = new ();
|
||||
|
||||
for (var i = 0; i < NUM_THREADS; i++)
|
||||
{
|
||||
int threadId = i;
|
||||
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Cycle through different key combinations
|
||||
switch (j % 6)
|
||||
{
|
||||
case 0:
|
||||
keyboard.QuitKey = Key.Q.WithCtrl;
|
||||
|
||||
break;
|
||||
case 1:
|
||||
keyboard.ArrangeKey = Key.F6.WithCtrl;
|
||||
|
||||
break;
|
||||
case 2:
|
||||
keyboard.NextTabKey = Key.Tab;
|
||||
|
||||
break;
|
||||
case 3:
|
||||
keyboard.PrevTabKey = Key.Tab.WithShift;
|
||||
|
||||
break;
|
||||
case 4:
|
||||
keyboard.NextTabGroupKey = Key.F6;
|
||||
|
||||
break;
|
||||
case 5:
|
||||
keyboard.PrevTabGroupKey = Key.F6.WithShift;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MixedOperations_ConcurrentAccess_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication? app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
var keyboard = new KeyboardImpl { App = app };
|
||||
keyboard.AddKeyBindings ();
|
||||
List<Exception> exceptions = [];
|
||||
const int OPERATIONS_PER_THREAD = 30;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = new ();
|
||||
|
||||
// Thread 1: Add bindings with unique keys
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use high key codes to avoid conflicts
|
||||
var key = new Key ((KeyCode)((int)KeyCode.F20 + j));
|
||||
keyboard.KeyBindings.Add (key, Command.Refresh);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Expected - duplicate
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Expected - invalid key
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Thread 2: Invoke commands
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
keyboard.InvokeCommandsBoundToKey (Key.Q.WithCtrl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Thread 3: Read bindings
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
IEnumerable<KeyValuePair<Key, KeyBinding>> bindings = keyboard.KeyBindings.GetBindings ();
|
||||
int count = bindings.Count ();
|
||||
Assert.True (count >= 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Thread 4: Change key properties
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
keyboard.QuitKey = j % 2 == 0 ? Key.Q.WithCtrl : Key.Esc;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
app.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaiseKeyDownEvent_ConcurrentAccess_NoExceptions ()
|
||||
{
|
||||
// Arrange
|
||||
IApplication? app = Application.Create ();
|
||||
app.Init ("fake");
|
||||
var keyboard = new KeyboardImpl { App = app };
|
||||
keyboard.AddKeyBindings ();
|
||||
List<Exception> exceptions = [];
|
||||
const int NUM_THREADS = 5;
|
||||
const int OPERATIONS_PER_THREAD = 20;
|
||||
|
||||
// Act
|
||||
List<Task> tasks = new ();
|
||||
|
||||
for (var i = 0; i < NUM_THREADS; i++)
|
||||
{
|
||||
tasks.Add (
|
||||
Task.Run (() =>
|
||||
{
|
||||
for (var j = 0; j < OPERATIONS_PER_THREAD; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
keyboard.RaiseKeyDownEvent (Key.A);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add (ex);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#pragma warning disable xUnit1031 // Test methods should not use blocking task operations - intentional for stress testing
|
||||
Task.WaitAll (tasks.ToArray ());
|
||||
#pragma warning restore xUnit1031
|
||||
|
||||
// Assert
|
||||
Assert.Empty (exceptions);
|
||||
keyboard.Dispose ();
|
||||
app.Dispose ();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
#nullable enable
|
||||
using Terminal.Gui.App;
|
||||
|
||||
namespace ApplicationTests.Keyboard;
|
||||
|
||||
/// <summary>
|
||||
/// Parallelizable tests for keyboard handling.
|
||||
/// These tests use isolated instances of <see cref="IKeyboard"/> to avoid static state dependencies.
|
||||
/// </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 ()
|
||||
{
|
||||
// Arrange & Act
|
||||
var keyboard = new KeyboardImpl ();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull (keyboard.KeyBindings);
|
||||
// Verify that some default bindings exist
|
||||
Assert.True (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuitKey_DefaultValue_IsEsc ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
|
||||
// Assert
|
||||
Assert.Equal (Key.Esc, keyboard.QuitKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuitKey_SetValue_UpdatesKeyBindings ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
Key newQuitKey = Key.Q.WithCtrl;
|
||||
|
||||
// Act
|
||||
keyboard.QuitKey = newQuitKey;
|
||||
|
||||
// Assert
|
||||
Assert.Equal (newQuitKey, keyboard.QuitKey);
|
||||
Assert.True (keyboard.KeyBindings.TryGet (newQuitKey, out KeyBinding binding));
|
||||
Assert.Contains (Command.Quit, binding.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrangeKey_DefaultValue_IsCtrlF5 ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
|
||||
// Assert
|
||||
Assert.Equal (Key.F5.WithCtrl, keyboard.ArrangeKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextTabKey_DefaultValue_IsTab ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
|
||||
// Assert
|
||||
Assert.Equal (Key.Tab, keyboard.NextTabKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrevTabKey_DefaultValue_IsShiftTab ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
|
||||
// Assert
|
||||
Assert.Equal (Key.Tab.WithShift, keyboard.PrevTabKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextTabGroupKey_DefaultValue_IsF6 ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
|
||||
// Assert
|
||||
Assert.Equal (Key.F6, keyboard.NextTabGroupKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrevTabGroupKey_DefaultValue_IsShiftF6 ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
|
||||
// Assert
|
||||
Assert.Equal (Key.F6.WithShift, keyboard.PrevTabGroupKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyBindings_Add_CanAddCustomBinding ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
Key customKey = Key.K.WithCtrl;
|
||||
|
||||
// Act
|
||||
keyboard.KeyBindings.Add (customKey, Command.Accept);
|
||||
|
||||
// Assert
|
||||
Assert.True (keyboard.KeyBindings.TryGet (customKey, out KeyBinding binding));
|
||||
Assert.Contains (Command.Accept, binding.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyBindings_Remove_CanRemoveBinding ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
Key customKey = Key.K.WithCtrl;
|
||||
keyboard.KeyBindings.Add (customKey, Command.Accept);
|
||||
|
||||
// Act
|
||||
keyboard.KeyBindings.Remove (customKey);
|
||||
|
||||
// Assert
|
||||
Assert.False (keyboard.KeyBindings.TryGet (customKey, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyDown_Event_CanBeSubscribed ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
bool eventRaised = false;
|
||||
|
||||
// Act
|
||||
keyboard.KeyDown += (sender, key) =>
|
||||
{
|
||||
eventRaised = true;
|
||||
};
|
||||
|
||||
// Assert - event subscription doesn't throw
|
||||
Assert.False (eventRaised); // Event hasn't been raised yet
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyUp_Event_CanBeSubscribed ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
bool eventRaised = false;
|
||||
|
||||
// Act
|
||||
keyboard.KeyUp += (sender, key) =>
|
||||
{
|
||||
eventRaised = true;
|
||||
};
|
||||
|
||||
// Assert - event subscription doesn't throw
|
||||
Assert.False (eventRaised); // Event hasn't been raised yet
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeCommand_WithInvalidCommand_ThrowsNotSupportedException ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
// Pick a command that isn't registered
|
||||
Command invalidCommand = (Command)9999;
|
||||
Key testKey = Key.A;
|
||||
var binding = new KeyBinding ([invalidCommand]);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<NotSupportedException> (() => keyboard.InvokeCommand (invalidCommand, testKey, binding));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Multiple_Keyboards_CanExistIndependently ()
|
||||
{
|
||||
// Arrange & Act
|
||||
var keyboard1 = new KeyboardImpl ();
|
||||
var keyboard2 = new KeyboardImpl ();
|
||||
|
||||
keyboard1.QuitKey = Key.Q.WithCtrl;
|
||||
keyboard2.QuitKey = Key.X.WithCtrl;
|
||||
|
||||
// Assert - each keyboard maintains independent state
|
||||
Assert.Equal (Key.Q.WithCtrl, keyboard1.QuitKey);
|
||||
Assert.Equal (Key.X.WithCtrl, keyboard2.QuitKey);
|
||||
Assert.NotEqual (keyboard1.QuitKey, keyboard2.QuitKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyBindings_Replace_UpdatesExistingBinding ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
Key oldKey = Key.Esc;
|
||||
Key newKey = Key.Q.WithCtrl;
|
||||
|
||||
// Verify initial state
|
||||
Assert.True (keyboard.KeyBindings.TryGet (oldKey, out KeyBinding oldBinding));
|
||||
Assert.Contains (Command.Quit, oldBinding.Commands);
|
||||
|
||||
// Act
|
||||
keyboard.KeyBindings.Replace (oldKey, newKey);
|
||||
|
||||
// Assert - old key should no longer have the binding
|
||||
Assert.False (keyboard.KeyBindings.TryGet (oldKey, out _));
|
||||
// New key should have the binding
|
||||
Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding newBinding));
|
||||
Assert.Contains (Command.Quit, newBinding.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyBindings_Clear_RemovesAllBindings ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
// Verify initial state has bindings
|
||||
Assert.True (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out _));
|
||||
|
||||
// Act
|
||||
keyboard.KeyBindings.Clear ();
|
||||
|
||||
// Assert - previously existing binding is gone
|
||||
Assert.False (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddKeyBindings_PopulatesDefaultBindings ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
keyboard.KeyBindings.Clear ();
|
||||
Assert.False (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out _));
|
||||
|
||||
// Act
|
||||
keyboard.AddKeyBindings ();
|
||||
|
||||
// Assert
|
||||
Assert.True (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out KeyBinding binding));
|
||||
Assert.Contains (Command.Quit, binding.Commands);
|
||||
}
|
||||
|
||||
// Migrated from UnitTests/Application/KeyboardTests.cs
|
||||
|
||||
[Fact]
|
||||
public void KeyBindings_Add_Adds ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
|
||||
// Act
|
||||
keyboard.KeyBindings.Add (Key.A, Command.Accept);
|
||||
keyboard.KeyBindings.Add (Key.B, Command.Accept);
|
||||
|
||||
// Assert
|
||||
Assert.True (keyboard.KeyBindings.TryGet (Key.A, out KeyBinding binding));
|
||||
Assert.Null (binding.Target);
|
||||
Assert.True (keyboard.KeyBindings.TryGet (Key.B, out binding));
|
||||
Assert.Null (binding.Target);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyBindings_Remove_Removes ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
keyboard.KeyBindings.Add (Key.A, Command.Accept);
|
||||
Assert.True (keyboard.KeyBindings.TryGet (Key.A, out _));
|
||||
|
||||
// Act
|
||||
keyboard.KeyBindings.Remove (Key.A);
|
||||
|
||||
// Assert
|
||||
Assert.False (keyboard.KeyBindings.TryGet (Key.A, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuitKey_Default_Is_Esc ()
|
||||
{
|
||||
// Arrange & Act
|
||||
var keyboard = new KeyboardImpl ();
|
||||
|
||||
// Assert
|
||||
Assert.Equal (Key.Esc, keyboard.QuitKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuitKey_Setter_UpdatesBindings ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
Key prevKey = keyboard.QuitKey;
|
||||
|
||||
// Act - Change QuitKey
|
||||
keyboard.QuitKey = Key.C.WithCtrl;
|
||||
|
||||
// Assert - Old key should no longer trigger quit
|
||||
Assert.False (keyboard.KeyBindings.TryGet (prevKey, out _));
|
||||
// New key should trigger quit
|
||||
Assert.True (keyboard.KeyBindings.TryGet (Key.C.WithCtrl, out KeyBinding binding));
|
||||
Assert.Contains (Command.Quit, binding.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextTabKey_Setter_UpdatesBindings ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
Key prevKey = keyboard.NextTabKey;
|
||||
Key newKey = Key.N.WithCtrl;
|
||||
|
||||
// Act
|
||||
keyboard.NextTabKey = newKey;
|
||||
|
||||
// Assert
|
||||
Assert.Equal (newKey, keyboard.NextTabKey);
|
||||
Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
|
||||
Assert.Contains (Command.NextTabStop, binding.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrevTabKey_Setter_UpdatesBindings ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
Key newKey = Key.P.WithCtrl;
|
||||
|
||||
// Act
|
||||
keyboard.PrevTabKey = newKey;
|
||||
|
||||
// Assert
|
||||
Assert.Equal (newKey, keyboard.PrevTabKey);
|
||||
Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
|
||||
Assert.Contains (Command.PreviousTabStop, binding.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextTabGroupKey_Setter_UpdatesBindings ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
Key newKey = Key.PageDown.WithCtrl;
|
||||
|
||||
// Act
|
||||
keyboard.NextTabGroupKey = newKey;
|
||||
|
||||
// Assert
|
||||
Assert.Equal (newKey, keyboard.NextTabGroupKey);
|
||||
Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, keyboard.NextTabGroupKey.KeyCode);
|
||||
Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
|
||||
Assert.Contains (Command.NextTabGroup, binding.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrevTabGroupKey_Setter_UpdatesBindings ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
Key newKey = Key.PageUp.WithCtrl;
|
||||
|
||||
// Act
|
||||
keyboard.PrevTabGroupKey = newKey;
|
||||
|
||||
// Assert
|
||||
Assert.Equal (newKey, keyboard.PrevTabGroupKey);
|
||||
Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, keyboard.PrevTabGroupKey.KeyCode);
|
||||
Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
|
||||
Assert.Contains (Command.PreviousTabGroup, binding.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrangeKey_Setter_UpdatesBindings ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
Key newKey = Key.A.WithCtrl;
|
||||
|
||||
// Act
|
||||
keyboard.ArrangeKey = newKey;
|
||||
|
||||
// Assert
|
||||
Assert.Equal (newKey, keyboard.ArrangeKey);
|
||||
Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
|
||||
Assert.Contains (Command.Arrange, binding.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyBindings_AddWithTarget_StoresTarget ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
var view = new View ();
|
||||
|
||||
// Act
|
||||
keyboard.KeyBindings.Add (Key.A.WithCtrl, view, Command.Accept);
|
||||
|
||||
// Assert
|
||||
Assert.True (keyboard.KeyBindings.TryGet (Key.A.WithCtrl, out KeyBinding binding));
|
||||
Assert.Equal (view, binding.Target);
|
||||
Assert.Contains (Command.Accept, binding.Commands);
|
||||
|
||||
view.Dispose ();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeCommandsBoundToKey_ReturnsNull_WhenNoBindingExists ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
Key unboundKey = Key.Z.WithAlt.WithCtrl;
|
||||
|
||||
// Act
|
||||
bool? result = keyboard.InvokeCommandsBoundToKey (unboundKey);
|
||||
|
||||
// Assert
|
||||
Assert.Null (result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeCommandsBoundToKey_InvokesCommand_WhenBindingExists ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
// QuitKey has a bound command by default
|
||||
|
||||
// Act
|
||||
bool? result = keyboard.InvokeCommandsBoundToKey (keyboard.QuitKey);
|
||||
|
||||
// Assert
|
||||
// Command.Quit would normally call Application.RequestStop,
|
||||
// but in isolation it should return true (handled)
|
||||
Assert.NotNull (result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Multiple_Keyboards_Independent_KeyBindings ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard1 = new KeyboardImpl ();
|
||||
var keyboard2 = new KeyboardImpl ();
|
||||
|
||||
// Act
|
||||
keyboard1.KeyBindings.Add (Key.X, Command.Accept);
|
||||
keyboard2.KeyBindings.Add (Key.Y, Command.Cancel);
|
||||
|
||||
// Assert
|
||||
Assert.True (keyboard1.KeyBindings.TryGet (Key.X, out _));
|
||||
Assert.False (keyboard1.KeyBindings.TryGet (Key.Y, out _));
|
||||
|
||||
Assert.True (keyboard2.KeyBindings.TryGet (Key.Y, out _));
|
||||
Assert.False (keyboard2.KeyBindings.TryGet (Key.X, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyBindings_Replace_PreservesCommandsForNewKey ()
|
||||
{
|
||||
// Arrange
|
||||
var keyboard = new KeyboardImpl ();
|
||||
Key oldKey = Key.Esc;
|
||||
Key newKey = Key.Q.WithCtrl;
|
||||
|
||||
// Get the commands from the old binding
|
||||
Assert.True (keyboard.KeyBindings.TryGet (oldKey, out KeyBinding oldBinding));
|
||||
Command [] oldCommands = oldBinding.Commands.ToArray ();
|
||||
|
||||
// Act
|
||||
keyboard.KeyBindings.Replace (oldKey, newKey);
|
||||
|
||||
// Assert - new key should have the same commands
|
||||
Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding newBinding));
|
||||
Assert.Equal (oldCommands, newBinding.Commands);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user