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

* Fixed MouseGrabView bug.

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

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

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

* Code cleanup.

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

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

* Improve null handling and simplify test setup

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

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

* Code cleanup

* API doc link cleanup

---------

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

View File

@@ -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 ();
}
}

View File

@@ -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);
}
}