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,798 @@
using System.Runtime.InteropServices;
namespace UnitTests_Parallelizable.DriverTests;
[Collection ("Global Test Setup")]
[Trait ("Platform", "Windows")]
public class WindowsKeyConverterTests
{
private readonly WindowsKeyConverter _converter = new ();
#region ToKey Tests - Basic Characters
[Theory]
[InlineData ('a', ConsoleKey.A, false, false, false, KeyCode.A)] // lowercase a
[InlineData ('A', ConsoleKey.A, true, false, false, KeyCode.A | KeyCode.ShiftMask)] // uppercase A
[InlineData ('z', ConsoleKey.Z, false, false, false, KeyCode.Z)]
[InlineData ('Z', ConsoleKey.Z, true, false, false, KeyCode.Z | KeyCode.ShiftMask)]
public void ToKey_LetterKeys_ReturnsExpectedKeyCode (
char unicodeChar,
ConsoleKey consoleKey,
bool shift,
bool alt,
bool ctrl,
KeyCode expectedKeyCode
)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
WindowsConsole.InputRecord inputRecord = CreateInputRecord (unicodeChar, consoleKey, shift, alt, ctrl);
// Act
var result = _converter.ToKey (inputRecord);
// Assert
Assert.Equal (expectedKeyCode, result.KeyCode);
}
[Theory]
[InlineData ('0', ConsoleKey.D0, false, false, false, KeyCode.D0)]
[InlineData ('1', ConsoleKey.D1, false, false, false, KeyCode.D1)]
[InlineData ('9', ConsoleKey.D9, false, false, false, KeyCode.D9)]
public void ToKey_NumberKeys_ReturnsExpectedKeyCode (
char unicodeChar,
ConsoleKey consoleKey,
bool shift,
bool alt,
bool ctrl,
KeyCode expectedKeyCode
)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
WindowsConsole.InputRecord inputRecord = CreateInputRecord (unicodeChar, consoleKey, shift, alt, ctrl);
// Act
var result = _converter.ToKey (inputRecord);
// Assert
Assert.Equal (expectedKeyCode, result.KeyCode);
}
#endregion
#region ToKey Tests - Modifiers
[Theory]
[InlineData ('a', ConsoleKey.A, false, false, true, KeyCode.A | KeyCode.CtrlMask)] // Ctrl+A
[InlineData ('A', ConsoleKey.A, true, false, true, KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask)] // Ctrl+Shift+A (Windows keeps ShiftMask)
[InlineData ('a', ConsoleKey.A, false, true, false, KeyCode.A | KeyCode.AltMask)] // Alt+A
[InlineData ('A', ConsoleKey.A, true, true, false, KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask)] // Alt+Shift+A
[InlineData ('a', ConsoleKey.A, false, true, true, KeyCode.A | KeyCode.CtrlMask | KeyCode.AltMask)] // Ctrl+Alt+A
public void ToKey_WithModifiers_ReturnsExpectedKeyCode (
char unicodeChar,
ConsoleKey consoleKey,
bool shift,
bool alt,
bool ctrl,
KeyCode expectedKeyCode
)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
WindowsConsole.InputRecord inputRecord = CreateInputRecord (unicodeChar, consoleKey, shift, alt, ctrl);
// Act
var result = _converter.ToKey (inputRecord);
// Assert
Assert.Equal (expectedKeyCode, result.KeyCode);
}
#endregion
#region ToKey Tests - Special Keys
[Theory]
[InlineData (ConsoleKey.Enter, KeyCode.Enter)]
[InlineData (ConsoleKey.Escape, KeyCode.Esc)]
[InlineData (ConsoleKey.Tab, KeyCode.Tab)]
[InlineData (ConsoleKey.Backspace, KeyCode.Backspace)]
[InlineData (ConsoleKey.Delete, KeyCode.Delete)]
[InlineData (ConsoleKey.Insert, KeyCode.Insert)]
[InlineData (ConsoleKey.Home, KeyCode.Home)]
[InlineData (ConsoleKey.End, KeyCode.End)]
[InlineData (ConsoleKey.PageUp, KeyCode.PageUp)]
[InlineData (ConsoleKey.PageDown, KeyCode.PageDown)]
[InlineData (ConsoleKey.UpArrow, KeyCode.CursorUp)]
[InlineData (ConsoleKey.DownArrow, KeyCode.CursorDown)]
[InlineData (ConsoleKey.LeftArrow, KeyCode.CursorLeft)]
[InlineData (ConsoleKey.RightArrow, KeyCode.CursorRight)]
public void ToKey_SpecialKeys_ReturnsExpectedKeyCode (ConsoleKey consoleKey, KeyCode expectedKeyCode)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
char unicodeChar = consoleKey switch
{
ConsoleKey.Enter => '\r',
ConsoleKey.Escape => '\u001B',
ConsoleKey.Tab => '\t',
ConsoleKey.Backspace => '\b',
_ => '\0'
};
WindowsConsole.InputRecord inputRecord = CreateInputRecord (unicodeChar, consoleKey, false, false, false);
// Act
var result = _converter.ToKey (inputRecord);
// Assert
Assert.Equal (expectedKeyCode, result.KeyCode);
}
[Theory]
[InlineData (ConsoleKey.F1, KeyCode.F1)]
[InlineData (ConsoleKey.F2, KeyCode.F2)]
[InlineData (ConsoleKey.F3, KeyCode.F3)]
[InlineData (ConsoleKey.F4, KeyCode.F4)]
[InlineData (ConsoleKey.F5, KeyCode.F5)]
[InlineData (ConsoleKey.F6, KeyCode.F6)]
[InlineData (ConsoleKey.F7, KeyCode.F7)]
[InlineData (ConsoleKey.F8, KeyCode.F8)]
[InlineData (ConsoleKey.F9, KeyCode.F9)]
[InlineData (ConsoleKey.F10, KeyCode.F10)]
[InlineData (ConsoleKey.F11, KeyCode.F11)]
[InlineData (ConsoleKey.F12, KeyCode.F12)]
public void ToKey_FunctionKeys_ReturnsExpectedKeyCode (ConsoleKey consoleKey, KeyCode expectedKeyCode)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
WindowsConsole.InputRecord inputRecord = CreateInputRecord ('\0', consoleKey, false, false, false);
// Act
var result = _converter.ToKey (inputRecord);
// Assert
Assert.Equal (expectedKeyCode, result.KeyCode);
}
#endregion
#region ToKey Tests - VK_PACKET (Unicode/IME)
[Theory]
[InlineData ('中')] // Chinese character
[InlineData ('日')] // Japanese character
[InlineData ('한')] // Korean character
[InlineData ('é')] // Accented character
[InlineData ('€')] // Euro symbol
[InlineData ('Ω')] // Greek character
public void ToKey_VKPacket_Unicode_ReturnsExpectedCharacter (char unicodeChar)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
WindowsConsole.InputRecord inputRecord = CreateVKPacketInputRecord (unicodeChar);
// Act
var result = _converter.ToKey (inputRecord);
// Assert
Assert.Equal ((KeyCode)unicodeChar, result.KeyCode);
}
[Fact]
public void ToKey_VKPacket_ZeroChar_ReturnsNull ()
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
WindowsConsole.InputRecord inputRecord = CreateVKPacketInputRecord ('\0');
// Act
var result = _converter.ToKey (inputRecord);
// Assert
Assert.Equal (KeyCode.Null, result.KeyCode);
}
[Fact]
public void ToKey_VKPacket_SurrogatePair_DocumentsCurrentLimitation ()
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Emoji '😀' (U+1F600) requires a surrogate pair: High=U+D83D, Low=U+DE00
// Windows sends this as TWO consecutive VK_PACKET events (one for each char)
// because KeyEventRecord.UnicodeChar is a single 16-bit char field.
//
// CURRENT LIMITATION: WindowsKeyConverter processes each event independently
// and does not combine surrogate pairs into a single Rune/KeyCode.
// This test documents the current (incorrect) behavior.
//
// TODO: Implement proper surrogate pair handling at the InputProcessor level
// to combine consecutive high+low surrogate events into a single Key with the
// complete Unicode codepoint.
// See: https://docs.microsoft.com/en-us/windows/console/key-event-record
var highSurrogate = '\uD83D'; // High surrogate for 😀
var lowSurrogate = '\uDE00'; // Low surrogate for 😀
// First event with high surrogate
WindowsConsole.InputRecord highRecord = CreateVKPacketInputRecord (highSurrogate);
var highResult = _converter.ToKey (highRecord);
// Second event with low surrogate
WindowsConsole.InputRecord lowRecord = CreateVKPacketInputRecord (lowSurrogate);
var lowResult = _converter.ToKey (lowRecord);
// Currently each surrogate half is processed independently as invalid KeyCodes
// These assertions document the current (broken) behavior
Assert.Equal ((KeyCode)highSurrogate, highResult.KeyCode);
Assert.Equal ((KeyCode)lowSurrogate, lowResult.KeyCode);
// What SHOULD happen (future fix):
// The InputProcessor should detect the surrogate pair and combine them:
// var expectedRune = new Rune(0x1F600); // 😀
// Assert.Equal((KeyCode)expectedRune.Value, combinedResult.KeyCode);
}
#endregion
#region ToKey Tests - OEM Keys
[Theory]
[InlineData (';', ConsoleKey.Oem1, false, (KeyCode)';')]
[InlineData (':', ConsoleKey.Oem1, true, (KeyCode)':')]
[InlineData ('/', ConsoleKey.Oem2, false, (KeyCode)'/')]
[InlineData ('?', ConsoleKey.Oem2, true, (KeyCode)'?')]
[InlineData (',', ConsoleKey.OemComma, false, (KeyCode)',')]
[InlineData ('<', ConsoleKey.OemComma, true, (KeyCode)'<')]
[InlineData ('.', ConsoleKey.OemPeriod, false, (KeyCode)'.')]
[InlineData ('>', ConsoleKey.OemPeriod, true, (KeyCode)'>')]
[InlineData ('=', ConsoleKey.OemPlus, false, (KeyCode)'=')] // Un-shifted OemPlus is '='
[InlineData ('+', ConsoleKey.OemPlus, true, (KeyCode)'+')] // Shifted OemPlus is '+'
[InlineData ('-', ConsoleKey.OemMinus, false, (KeyCode)'-')]
[InlineData ('_', ConsoleKey.OemMinus, true, (KeyCode)'_')] // Shifted OemMinus is '_'
public void ToKey_OEMKeys_ReturnsExpectedKeyCode (
char unicodeChar,
ConsoleKey consoleKey,
bool shift,
KeyCode expectedKeyCode
)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
WindowsConsole.InputRecord inputRecord = CreateInputRecord (unicodeChar, consoleKey, shift, false, false);
// Act
var result = _converter.ToKey (inputRecord);
// Assert
Assert.Equal (expectedKeyCode, result.KeyCode);
}
#endregion
#region ToKey Tests - NumPad
[Theory]
[InlineData ('0', ConsoleKey.NumPad0, KeyCode.D0)]
[InlineData ('1', ConsoleKey.NumPad1, KeyCode.D1)]
[InlineData ('5', ConsoleKey.NumPad5, KeyCode.D5)]
[InlineData ('9', ConsoleKey.NumPad9, KeyCode.D9)]
public void ToKey_NumPadKeys_ReturnsExpectedKeyCode (
char unicodeChar,
ConsoleKey consoleKey,
KeyCode expectedKeyCode
)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
WindowsConsole.InputRecord inputRecord = CreateInputRecord (unicodeChar, consoleKey, false, false, false);
// Act
var result = _converter.ToKey (inputRecord);
// Assert
Assert.Equal (expectedKeyCode, result.KeyCode);
}
[Theory]
[InlineData ('*', ConsoleKey.Multiply, (KeyCode)'*')]
[InlineData ('+', ConsoleKey.Add, (KeyCode)'+')]
[InlineData ('-', ConsoleKey.Subtract, (KeyCode)'-')]
[InlineData ('.', ConsoleKey.Decimal, (KeyCode)'.')]
[InlineData ('/', ConsoleKey.Divide, (KeyCode)'/')]
public void ToKey_NumPadOperators_ReturnsExpectedKeyCode (
char unicodeChar,
ConsoleKey consoleKey,
KeyCode expectedKeyCode
)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
WindowsConsole.InputRecord inputRecord = CreateInputRecord (unicodeChar, consoleKey, false, false, false);
// Act
var result = _converter.ToKey (inputRecord);
// Assert
Assert.Equal (expectedKeyCode, result.KeyCode);
}
#endregion
#region ToKey Tests - Null/Empty
[Fact]
public void ToKey_NullKey_ReturnsEmpty ()
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
WindowsConsole.InputRecord inputRecord = CreateInputRecord ('\0', ConsoleKey.None, false, false, false);
// Act
var result = _converter.ToKey (inputRecord);
// Assert
Assert.Equal (Key.Empty, result);
}
#endregion
#region ToKeyInfo Tests - Basic Keys
[Theory]
[InlineData (KeyCode.A, ConsoleKey.A, 'a')]
[InlineData (KeyCode.A | KeyCode.ShiftMask, ConsoleKey.A, 'A')]
[InlineData (KeyCode.Z, ConsoleKey.Z, 'z')]
[InlineData (KeyCode.Z | KeyCode.ShiftMask, ConsoleKey.Z, 'Z')]
public void ToKeyInfo_LetterKeys_ReturnsExpectedInputRecord (
KeyCode keyCode,
ConsoleKey expectedConsoleKey,
char expectedChar
)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
var key = new Key (keyCode);
// Act
WindowsConsole.InputRecord result = _converter.ToKeyInfo (key);
// Assert
Assert.Equal (WindowsConsole.EventType.Key, result.EventType);
Assert.Equal ((VK)expectedConsoleKey, result.KeyEvent.wVirtualKeyCode);
Assert.Equal (expectedChar, result.KeyEvent.UnicodeChar);
Assert.True (result.KeyEvent.bKeyDown);
Assert.Equal ((ushort)1, result.KeyEvent.wRepeatCount);
}
[Theory]
[InlineData (KeyCode.D0, ConsoleKey.D0, '0')]
[InlineData (KeyCode.D1, ConsoleKey.D1, '1')]
[InlineData (KeyCode.D9, ConsoleKey.D9, '9')]
public void ToKeyInfo_NumberKeys_ReturnsExpectedInputRecord (
KeyCode keyCode,
ConsoleKey expectedConsoleKey,
char expectedChar
)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
var key = new Key (keyCode);
// Act
WindowsConsole.InputRecord result = _converter.ToKeyInfo (key);
// Assert
Assert.Equal ((VK)expectedConsoleKey, result.KeyEvent.wVirtualKeyCode);
Assert.Equal (expectedChar, result.KeyEvent.UnicodeChar);
}
#endregion
#region ToKeyInfo Tests - Special Keys
[Theory]
[InlineData (KeyCode.Enter, ConsoleKey.Enter, '\r')]
[InlineData (KeyCode.Esc, ConsoleKey.Escape, '\u001B')]
[InlineData (KeyCode.Tab, ConsoleKey.Tab, '\t')]
[InlineData (KeyCode.Backspace, ConsoleKey.Backspace, '\b')]
[InlineData (KeyCode.Space, ConsoleKey.Spacebar, ' ')]
public void ToKeyInfo_SpecialKeys_ReturnsExpectedInputRecord (
KeyCode keyCode,
ConsoleKey expectedConsoleKey,
char expectedChar
)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
var key = new Key (keyCode);
// Act
WindowsConsole.InputRecord result = _converter.ToKeyInfo (key);
// Assert
Assert.Equal ((VK)expectedConsoleKey, result.KeyEvent.wVirtualKeyCode);
Assert.Equal (expectedChar, result.KeyEvent.UnicodeChar);
}
[Theory]
[InlineData (KeyCode.Delete, ConsoleKey.Delete)]
[InlineData (KeyCode.Insert, ConsoleKey.Insert)]
[InlineData (KeyCode.Home, ConsoleKey.Home)]
[InlineData (KeyCode.End, ConsoleKey.End)]
[InlineData (KeyCode.PageUp, ConsoleKey.PageUp)]
[InlineData (KeyCode.PageDown, ConsoleKey.PageDown)]
[InlineData (KeyCode.CursorUp, ConsoleKey.UpArrow)]
[InlineData (KeyCode.CursorDown, ConsoleKey.DownArrow)]
[InlineData (KeyCode.CursorLeft, ConsoleKey.LeftArrow)]
[InlineData (KeyCode.CursorRight, ConsoleKey.RightArrow)]
public void ToKeyInfo_NavigationKeys_ReturnsExpectedInputRecord (KeyCode keyCode, ConsoleKey expectedConsoleKey)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
var key = new Key (keyCode);
// Act
WindowsConsole.InputRecord result = _converter.ToKeyInfo (key);
// Assert
Assert.Equal ((VK)expectedConsoleKey, result.KeyEvent.wVirtualKeyCode);
}
[Theory]
[InlineData (KeyCode.F1, ConsoleKey.F1)]
[InlineData (KeyCode.F5, ConsoleKey.F5)]
[InlineData (KeyCode.F10, ConsoleKey.F10)]
[InlineData (KeyCode.F12, ConsoleKey.F12)]
public void ToKeyInfo_FunctionKeys_ReturnsExpectedInputRecord (KeyCode keyCode, ConsoleKey expectedConsoleKey)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
var key = new Key (keyCode);
// Act
WindowsConsole.InputRecord result = _converter.ToKeyInfo (key);
// Assert
Assert.Equal ((VK)expectedConsoleKey, result.KeyEvent.wVirtualKeyCode);
}
#endregion
#region ToKeyInfo Tests - Modifiers
[Theory]
[InlineData (KeyCode.A | KeyCode.ShiftMask, WindowsConsole.ControlKeyState.ShiftPressed)]
[InlineData (KeyCode.A | KeyCode.CtrlMask, WindowsConsole.ControlKeyState.LeftControlPressed)]
[InlineData (KeyCode.A | KeyCode.AltMask, WindowsConsole.ControlKeyState.LeftAltPressed)]
[InlineData (
KeyCode.A | KeyCode.CtrlMask | KeyCode.AltMask,
WindowsConsole.ControlKeyState.LeftControlPressed | WindowsConsole.ControlKeyState.LeftAltPressed)]
[InlineData (
KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask,
WindowsConsole.ControlKeyState.ShiftPressed
| WindowsConsole.ControlKeyState.LeftControlPressed
| WindowsConsole.ControlKeyState.LeftAltPressed)]
public void ToKeyInfo_WithModifiers_ReturnsExpectedControlKeyState (
KeyCode keyCode,
WindowsConsole.ControlKeyState expectedState
)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
var key = new Key (keyCode);
// Act
WindowsConsole.InputRecord result = _converter.ToKeyInfo (key);
// Assert
Assert.Equal (expectedState, result.KeyEvent.dwControlKeyState);
}
#endregion
#region ToKeyInfo Tests - Scan Codes
[Theory]
[InlineData (KeyCode.A, 30)]
[InlineData (KeyCode.Enter, 28)]
[InlineData (KeyCode.Esc, 1)]
[InlineData (KeyCode.Space, 57)]
[InlineData (KeyCode.F1, 59)]
[InlineData (KeyCode.F10, 68)]
[InlineData (KeyCode.CursorUp, 72)]
[InlineData (KeyCode.Home, 71)]
public void ToKeyInfo_ScanCodes_ReturnsExpectedScanCode (KeyCode keyCode, ushort expectedScanCode)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
var key = new Key (keyCode);
// Act
WindowsConsole.InputRecord result = _converter.ToKeyInfo (key);
// Assert
Assert.Equal (expectedScanCode, result.KeyEvent.wVirtualScanCode);
}
#endregion
#region Round-Trip Tests
[Theory]
[InlineData (KeyCode.A)]
[InlineData (KeyCode.A | KeyCode.ShiftMask)]
[InlineData (KeyCode.A | KeyCode.CtrlMask)]
[InlineData (KeyCode.Enter)]
[InlineData (KeyCode.F1)]
[InlineData (KeyCode.CursorUp)]
[InlineData (KeyCode.Delete)]
[InlineData (KeyCode.D5)]
[InlineData (KeyCode.Space)]
public void RoundTrip_ToKeyInfo_ToKey_PreservesKeyCode (KeyCode originalKeyCode)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
var originalKey = new Key (originalKeyCode);
// Act
WindowsConsole.InputRecord inputRecord = _converter.ToKeyInfo (originalKey);
var roundTrippedKey = _converter.ToKey (inputRecord);
// Assert
Assert.Equal (originalKeyCode, roundTrippedKey.KeyCode);
}
[Theory]
[InlineData ('a', ConsoleKey.A, false, false, false)]
[InlineData ('A', ConsoleKey.A, true, false, false)]
[InlineData ('a', ConsoleKey.A, false, false, true)] // Ctrl+A
[InlineData ('0', ConsoleKey.D0, false, false, false)]
public void RoundTrip_ToKey_ToKeyInfo_PreservesData (
char unicodeChar,
ConsoleKey consoleKey,
bool shift,
bool alt,
bool ctrl
)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
// Arrange
WindowsConsole.InputRecord originalRecord = CreateInputRecord (unicodeChar, consoleKey, shift, alt, ctrl);
// Act
var key = _converter.ToKey (originalRecord);
WindowsConsole.InputRecord roundTrippedRecord = _converter.ToKeyInfo (key);
// Assert
Assert.Equal ((VK)consoleKey, roundTrippedRecord.KeyEvent.wVirtualKeyCode);
// Check modifiers match
var expectedState = WindowsConsole.ControlKeyState.NoControlKeyPressed;
if (shift)
{
expectedState |= WindowsConsole.ControlKeyState.ShiftPressed;
}
if (alt)
{
expectedState |= WindowsConsole.ControlKeyState.LeftAltPressed;
}
if (ctrl)
{
expectedState |= WindowsConsole.ControlKeyState.LeftControlPressed;
}
Assert.True (roundTrippedRecord.KeyEvent.dwControlKeyState.HasFlag (expectedState));
}
#endregion
#region CapsLock/NumLock Tests
[Theory]
[InlineData ('a', ConsoleKey.A, false, true)] // CapsLock on, no shift
[InlineData ('A', ConsoleKey.A, true, true)] // CapsLock on, shift (should be lowercase from mapping)
public void ToKey_WithCapsLock_ReturnsExpectedKeyCode (
char unicodeChar,
ConsoleKey consoleKey,
bool shift,
bool capsLock
)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
}
WindowsConsole.InputRecord inputRecord = CreateInputRecordWithLockStates (
unicodeChar,
consoleKey,
shift,
false,
false,
capsLock,
false,
false);
// Act
var result = _converter.ToKey (inputRecord);
// Assert
// The mapping should handle CapsLock properly via WindowsKeyHelper.MapKey
Assert.NotEqual (KeyCode.Null, result.KeyCode);
}
#endregion
#region Helper Methods
private static WindowsConsole.InputRecord CreateInputRecord (
char unicodeChar,
ConsoleKey consoleKey,
bool shift,
bool alt,
bool ctrl
) =>
CreateInputRecordWithLockStates (unicodeChar, consoleKey, shift, alt, ctrl, false, false, false);
private static WindowsConsole.InputRecord CreateInputRecordWithLockStates (
char unicodeChar,
ConsoleKey consoleKey,
bool shift,
bool alt,
bool ctrl,
bool capsLock,
bool numLock,
bool scrollLock
)
{
var controlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed;
if (shift)
{
controlKeyState |= WindowsConsole.ControlKeyState.ShiftPressed;
}
if (alt)
{
controlKeyState |= WindowsConsole.ControlKeyState.LeftAltPressed;
}
if (ctrl)
{
controlKeyState |= WindowsConsole.ControlKeyState.LeftControlPressed;
}
if (capsLock)
{
controlKeyState |= WindowsConsole.ControlKeyState.CapslockOn;
}
if (numLock)
{
controlKeyState |= WindowsConsole.ControlKeyState.NumlockOn;
}
if (scrollLock)
{
controlKeyState |= WindowsConsole.ControlKeyState.ScrolllockOn;
}
return new ()
{
EventType = WindowsConsole.EventType.Key,
KeyEvent = new ()
{
bKeyDown = true,
wRepeatCount = 1,
wVirtualKeyCode = (VK)consoleKey,
wVirtualScanCode = 0,
UnicodeChar = unicodeChar,
dwControlKeyState = controlKeyState
}
};
}
private static WindowsConsole.InputRecord CreateVKPacketInputRecord (char unicodeChar) =>
new ()
{
EventType = WindowsConsole.EventType.Key,
KeyEvent = new ()
{
bKeyDown = true,
wRepeatCount = 1,
wVirtualKeyCode = VK.PACKET,
wVirtualScanCode = 0,
UnicodeChar = unicodeChar,
dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
}
};
#endregion
}