Fixes #4389 - Add comprehensive unit tests for WindowsKeyConverter (#4390)

* Add comprehensive unit tests for WindowsKeyConverter

- Implement 118 parallelizable unit tests for WindowsKeyConverter

- Cover ToKey and ToKeyInfo methods with full bidirectional testing

- Test basic characters, modifiers, special keys, function keys

- Test VK_PACKET Unicode/IME input

- Test OEM keys, NumPad keys, and lock states

- Include round-trip conversion tests

- All tests passing successfully

Fixes #4389

* Add test documenting VK_PACKET surrogate pair limitation

VK_PACKET sends surrogate pairs (e.g., emoji) as two separate events. Current WindowsKeyConverter processes each independently without combining. Test documents this limitation for future fix at InputProcessor level.

* Mark WindowsKeyConverterTests as Windows-only

Tests now skip on non-Windows platforms using SkipException in constructor. This prevents CI failures on macOS and Linux where Windows Console API is not available.

* Properly mark WindowsKeyConverter tests as Windows-only

Created custom xUnit attributes SkipOnNonWindowsFact and SkipOnNonWindowsTheory that automatically skip tests on non-Windows platforms. This prevents CI failures on macOS and Linux.

* Refactor and enhance test coverage for input processors

Refactored `ApplicationImpl.cs` to simplify its structure by removing the `_stopAfterFirstIteration` field. Reintroduced and modernized test files with improved structure and coverage:

- `NetInputProcessorTests.cs`: Added tests for `ConsoleKeyInfo` to `Rune`/`Key` conversion and queue processing.
- `WindowSizeMonitorTests.cs`: Added tests for size change event handling.
- `WindowsInputProcessorTests.cs`: Added tests for keyboard and mouse input processing, including mouse flag mapping.
- `WindowsKeyConverterTests.cs`: Added comprehensive tests for `InputRecord` to `Key` conversion, covering OEM keys, modifiers, Unicode, and round-trip integrity.

Improved test coverage for edge cases, introduced parameterized tests, and documented known limitations for future improvements.

* Use Trait-based platform filtering for WindowsKeyConverter tests

Added [Trait('Platform', 'Windows')] and [Collection('Global Test Setup')] attributes. Tests will run on Windows but can be filtered in CI on other platforms using --filter 'Platform!=Windows' if needed. This approach doesn't interfere with GlobalTestSetup and works correctly with xUnit.

* Filter Windows-specific tests on non-Windows CI platforms

Added --filter 'Platform!=Windows' for Linux and macOS runners to exclude WindowsKeyConverterTests which require Windows Console APIs. Windows runner runs all tests normally.

* Fix log path typo and remove Codecov upload step

Corrected a typo in the log directory path from
`logs/UnitTestsParallelable/` to `logs/UnitTestsParallelizable/`.
Removed the "Upload Parallelizable UnitTests Coverage to Codecov"
step, which was conditional on `matrix.os == 'ubuntu-latest'`
and used the `codecov/codecov-action@v4` action. This change
improves log handling and removes the Codecov integration.

* Refactor application reset logic

Replaced `Application.ResetState(true)` with a more explicit reset
mechanism. Introduced `ApplicationImpl.SetInstance(null)` to clear
the application instance and added `CM.Disable(true)` to disable
specific components. This change improves control over the reset
process and ensures a more granular approach to application state
management.

* Improve null safety with ?. and ! operators

Enhanced null safety across the codebase by introducing the null-conditional operator (`?.`) and null-forgiving operator (`!`) where appropriate.

- Updated `app` and `driver` method calls to use `?.` to prevent potential `NullReferenceException` errors.
- Added `!` to assert non-nullability in cases like `e.Value!.ToString()` and `app.Driver?.Contents!`.
- Modified `lv.SelectedItemChanged` event handler to ensure safe handling of nullable values.
- Updated `app.Shutdown()`, `app.LayoutAndDraw()`, and mouse event handling to use `?.`.
- Ensured `driver.SetScreenSize` is invoked only when `driver` is not null.
- Improved string concatenation logic with null-forgiving operator for `Contents`.
- General improvements to null safety to make the code more robust and resilient to null references.

* Improve Unicode tests and clarify surrogate pair handling

Updated `WindowsKeyConverterTests` to enhance readability, improve test data, and clarify comments. Key changes include:

- Reformatted `[Collection]` and `[Trait]` attributes for consistency.
- Replaced placeholder Unicode characters with meaningful examples:
  - Chinese: `中`, Japanese: `日`, Korean: `한`, Accented: `é`, Euro: `€`, Greek: `Ω`.
- Updated comments to replace placeholder emojis (`??`) with `😀` (U+1F600) for clarity.
- Adjusted surrogate pair test data to accurately reflect `😀`.
- Improved documentation of current limitations and future fixes for surrogate pair handling.

These changes ensure more accurate and meaningful test cases while improving code clarity.

* Ensure platform-specific test execution on Windows

Added `System.Runtime.InteropServices` and `Xunit.Sdk` namespaces to `WindowsKeyConverterTests.cs` to support platform checks and test setup. Marked the test class with `[Collection("Global Test Setup")]` to enable shared test setup. Updated the `ToKey_NumPadKeys_ReturnsExpectedKeyCode` method to include a platform check, ensuring the test only runs on Windows platforms.

* Integrate TrueColor support into ColorPicker

Merged TrueColors functionality into ColorPicker, enhancing the scenario with TrueColor demonstration and gradient features. Updated `ColorPicker.cs` to include driver information, TrueColor support indicators, and a toggle for `Force16Colors`. Removed `TrueColors.cs` as its functionality is now consolidated.

Refined `ColorBar` to use dynamic height with `Dim.Auto` for better flexibility. Added documentation to `HueBar` to clarify its role in representing the Hue component in HSL color space.

* Revert workflow change.

* Reverted attribute that didn't actualy work.
This commit is contained in:
Tig
2025-11-20 13:20:01 -05:00
committed by GitHub
parent 1bd5e3761a
commit 726b15dd28
11 changed files with 848 additions and 183 deletions

View File

@@ -0,0 +1,119 @@
using System.Collections.Concurrent;
using System.Text;
namespace UnitTests_Parallelizable.DriverTests;
public class NetInputProcessorTests
{
public static IEnumerable<object []> GetConsoleKeyInfoToKeyTestCases_Rune ()
{
yield return [new ConsoleKeyInfo ('C', ConsoleKey.None, false, false, false), new Rune ('C')];
yield return [new ConsoleKeyInfo ('\\', ConsoleKey.Oem5, false, false, false), new Rune ('\\')];
yield return [new ConsoleKeyInfo ('+', ConsoleKey.OemPlus, true, false, false), new Rune ('+')];
yield return [new ConsoleKeyInfo ('=', ConsoleKey.OemPlus, false, false, false), new Rune ('=')];
yield return new object [] { new ConsoleKeyInfo ('_', ConsoleKey.OemMinus, true, false, false), new Rune ('_') };
yield return new object [] { new ConsoleKeyInfo ('-', ConsoleKey.OemMinus, false, false, false), new Rune ('-') };
yield return new object [] { new ConsoleKeyInfo (')', ConsoleKey.None, false, false, false), new Rune (')') };
yield return new object [] { new ConsoleKeyInfo ('0', ConsoleKey.None, false, false, false), new Rune ('0') };
yield return new object [] { new ConsoleKeyInfo ('(', ConsoleKey.None, false, false, false), new Rune ('(') };
yield return new object [] { new ConsoleKeyInfo ('9', ConsoleKey.None, false, false, false), new Rune ('9') };
yield return new object [] { new ConsoleKeyInfo ('*', ConsoleKey.None, false, false, false), new Rune ('*') };
yield return new object [] { new ConsoleKeyInfo ('8', ConsoleKey.None, false, false, false), new Rune ('8') };
yield return new object [] { new ConsoleKeyInfo ('&', ConsoleKey.None, false, false, false), new Rune ('&') };
yield return new object [] { new ConsoleKeyInfo ('7', ConsoleKey.None, false, false, false), new Rune ('7') };
yield return new object [] { new ConsoleKeyInfo ('^', ConsoleKey.None, false, false, false), new Rune ('^') };
yield return new object [] { new ConsoleKeyInfo ('6', ConsoleKey.None, false, false, false), new Rune ('6') };
yield return new object [] { new ConsoleKeyInfo ('%', ConsoleKey.None, false, false, false), new Rune ('%') };
yield return new object [] { new ConsoleKeyInfo ('5', ConsoleKey.None, false, false, false), new Rune ('5') };
yield return new object [] { new ConsoleKeyInfo ('$', ConsoleKey.None, false, false, false), new Rune ('$') };
yield return new object [] { new ConsoleKeyInfo ('4', ConsoleKey.None, false, false, false), new Rune ('4') };
yield return new object [] { new ConsoleKeyInfo ('#', ConsoleKey.None, false, false, false), new Rune ('#') };
yield return new object [] { new ConsoleKeyInfo ('@', ConsoleKey.None, false, false, false), new Rune ('@') };
yield return new object [] { new ConsoleKeyInfo ('2', ConsoleKey.None, false, false, false), new Rune ('2') };
yield return new object [] { new ConsoleKeyInfo ('!', ConsoleKey.None, false, false, false), new Rune ('!') };
yield return new object [] { new ConsoleKeyInfo ('1', ConsoleKey.None, false, false, false), new Rune ('1') };
yield return new object [] { new ConsoleKeyInfo ('\t', ConsoleKey.None, false, false, false), new Rune ('\t') };
yield return new object [] { new ConsoleKeyInfo ('}', ConsoleKey.Oem6, true, false, false), new Rune ('}') };
yield return new object [] { new ConsoleKeyInfo (']', ConsoleKey.Oem6, false, false, false), new Rune (']') };
yield return new object [] { new ConsoleKeyInfo ('{', ConsoleKey.Oem4, true, false, false), new Rune ('{') };
yield return new object [] { new ConsoleKeyInfo ('[', ConsoleKey.Oem4, false, false, false), new Rune ('[') };
yield return new object [] { new ConsoleKeyInfo ('\"', ConsoleKey.Oem7, true, false, false), new Rune ('\"') };
yield return new object [] { new ConsoleKeyInfo ('\'', ConsoleKey.Oem7, false, false, false), new Rune ('\'') };
yield return new object [] { new ConsoleKeyInfo (':', ConsoleKey.Oem1, true, false, false), new Rune (':') };
yield return new object [] { new ConsoleKeyInfo (';', ConsoleKey.Oem1, false, false, false), new Rune (';') };
yield return new object [] { new ConsoleKeyInfo ('?', ConsoleKey.Oem2, true, false, false), new Rune ('?') };
yield return new object [] { new ConsoleKeyInfo ('/', ConsoleKey.Oem2, false, false, false), new Rune ('/') };
yield return new object [] { new ConsoleKeyInfo ('>', ConsoleKey.OemPeriod, true, false, false), new Rune ('>') };
yield return new object [] { new ConsoleKeyInfo ('.', ConsoleKey.OemPeriod, false, false, false), new Rune ('.') };
yield return new object [] { new ConsoleKeyInfo ('<', ConsoleKey.OemComma, true, false, false), new Rune ('<') };
yield return new object [] { new ConsoleKeyInfo (',', ConsoleKey.OemComma, false, false, false), new Rune (',') };
yield return new object [] { new ConsoleKeyInfo ('w', ConsoleKey.None, false, false, false), new Rune ('w') };
yield return new object [] { new ConsoleKeyInfo ('e', ConsoleKey.None, false, false, false), new Rune ('e') };
yield return new object [] { new ConsoleKeyInfo ('a', ConsoleKey.None, false, false, false), new Rune ('a') };
yield return new object [] { new ConsoleKeyInfo ('s', ConsoleKey.None, false, false, false), new Rune ('s') };
}
[Theory]
[MemberData (nameof (GetConsoleKeyInfoToKeyTestCases_Rune))]
public void ConsoleKeyInfoToKey_ValidInput_AsRune (ConsoleKeyInfo input, Rune expected)
{
var converter = new NetKeyConverter ();
// Act
var result = converter.ToKey (input);
// Assert
Assert.Equal (expected, result.AsRune);
}
public static IEnumerable<object []> GetConsoleKeyInfoToKeyTestCases_Key ()
{
yield return new object [] { new ConsoleKeyInfo ('\t', ConsoleKey.None, false, false, false), Key.Tab };
yield return new object [] { new ConsoleKeyInfo ('\u001B', ConsoleKey.None, false, false, false), Key.Esc };
yield return new object [] { new ConsoleKeyInfo ('\u007f', ConsoleKey.None, false, false, false), Key.Backspace };
// TODO: Terminal.Gui does not have a Key for this mapped
// TODO: null and default(Key) are both not same as Null. Why user has to do (Key)0 to get a null key?!
yield return new object [] { new ConsoleKeyInfo ('\0', ConsoleKey.LeftWindows, false, false, false), (Key)0 };
}
[Theory]
[MemberData (nameof (GetConsoleKeyInfoToKeyTestCases_Key))]
public void ConsoleKeyInfoToKey_ValidInput_AsKey (ConsoleKeyInfo input, Key expected)
{
var converter = new NetKeyConverter ();
// Act
var result = converter.ToKey (input);
// Assert
Assert.Equal (expected, result);
}
[Fact]
public void Test_ProcessQueue_CapitalHLowerE ()
{
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
queue.Enqueue (new ('H', ConsoleKey.None, true, false, false));
queue.Enqueue (new ('e', ConsoleKey.None, false, false, false));
var processor = new NetInputProcessor (queue);
List<Key> ups = new ();
List<Key> downs = new ();
processor.KeyUp += (s, e) => { ups.Add (e); };
processor.KeyDown += (s, e) => { downs.Add (e); };
Assert.Empty (ups);
Assert.Empty (downs);
processor.ProcessQueue ();
Assert.Equal (Key.H.WithShift, ups [0]);
Assert.Equal (Key.H.WithShift, downs [0]);
Assert.Equal (Key.E, ups [1]);
Assert.Equal (Key.E, downs [1]);
}
}