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

@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace UICatalog.Scenarios;
[ScenarioMetadata ("ColorPicker", "Color Picker.")]
[ScenarioMetadata ("ColorPicker", "Color Picker and TrueColor demonstration.")]
[ScenarioCategory ("Colors")]
[ScenarioCategory ("Controls")]
public class ColorPickers : Scenario
@@ -220,6 +220,33 @@ public class ColorPickers : Scenario
};
app.Add (cbShowName);
var lblDriverName = new Label
{
Y = Pos.Bottom (cbShowName) + 1, Text = $"Driver is `{Application.Driver?.GetName ()}`:"
};
bool canTrueColor = Application.Driver?.SupportsTrueColor ?? false;
var cbSupportsTrueColor = new CheckBox
{
X = Pos.Right (lblDriverName) + 1,
Y = Pos.Top (lblDriverName),
CheckedState = canTrueColor ? CheckState.Checked : CheckState.UnChecked,
CanFocus = false,
Enabled = false,
Text = "SupportsTrueColor"
};
app.Add (cbSupportsTrueColor);
var cbUseTrueColor = new CheckBox
{
X = Pos.Right (cbSupportsTrueColor) + 1,
Y = Pos.Top (lblDriverName),
CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
Enabled = canTrueColor,
Text = "Force16Colors"
};
cbUseTrueColor.CheckedStateChanging += (_, evt) => { Application.Force16Colors = evt.Result == CheckState.Checked; };
app.Add (lblDriverName, cbSupportsTrueColor, cbUseTrueColor);
// Set default colors.
foregroundColorPicker.SelectedColor = _demoView.SuperView!.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ();
backgroundColorPicker.SelectedColor = _demoView.SuperView.GetAttributeForRole (VisualRole.Normal).Background.GetClosestNamedColor16 ();

View File

@@ -1,157 +0,0 @@
using System;
namespace UICatalog.Scenarios;
[ScenarioMetadata ("True Colors", "Demonstration of true color support.")]
[ScenarioCategory ("Colors")]
public class TrueColors : Scenario
{
public override void Main ()
{
Application.Init ();
Window app = new ()
{
Title = GetQuitKeyAndName ()
};
var x = 2;
var y = 1;
bool canTrueColor = Application.Driver?.SupportsTrueColor ?? false;
var lblDriverName = new Label
{
X = x, Y = y++, Text = $"Current driver is {Application.Driver?.GetType ().Name}"
};
app.Add (lblDriverName);
y++;
var cbSupportsTrueColor = new CheckBox
{
X = x,
Y = y++,
CheckedState = canTrueColor ? CheckState.Checked : CheckState.UnChecked,
CanFocus = false,
Enabled = false,
Text = "Driver supports true color "
};
app.Add (cbSupportsTrueColor);
var cbUseTrueColor = new CheckBox
{
X = x,
Y = y++,
CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
Enabled = canTrueColor,
Text = "Force 16 colors"
};
cbUseTrueColor.CheckedStateChanging += (_, evt) => { Application.Force16Colors = evt.Result == CheckState.Checked; };
app.Add (cbUseTrueColor);
y += 2;
SetupGradient ("Red gradient", x, ref y, i => new (i, 0));
SetupGradient ("Green gradient", x, ref y, i => new (0, i));
SetupGradient ("Blue gradient", x, ref y, i => new (0, 0, i));
SetupGradient ("Yellow gradient", x, ref y, i => new (i, i));
SetupGradient ("Magenta gradient", x, ref y, i => new (i, 0, i));
SetupGradient ("Cyan gradient", x, ref y, i => new (0, i, i));
SetupGradient ("Gray gradient", x, ref y, i => new (i, i, i));
app.Add (
new Label { X = Pos.AnchorEnd (44), Y = 2, Text = "Mouse over to get the gradient view color:" }
);
app.Add (
new Label { X = Pos.AnchorEnd (44), Y = 4, Text = "Red:" }
);
app.Add (
new Label { X = Pos.AnchorEnd (44), Y = 5, Text = "Green:" }
);
app.Add (
new Label { X = Pos.AnchorEnd (44), Y = 6, Text = "Blue:" }
);
app.Add (
new Label { X = Pos.AnchorEnd (44), Y = 8, Text = "Darker:" }
);
app.Add (
new Label { X = Pos.AnchorEnd (44), Y = 9, Text = "Lighter:" }
);
var lblRed = new Label { X = Pos.AnchorEnd (32), Y = 4, Text = "na" };
app.Add (lblRed);
var lblGreen = new Label { X = Pos.AnchorEnd (32), Y = 5, Text = "na" };
app.Add (lblGreen);
var lblBlue = new Label { X = Pos.AnchorEnd (32), Y = 6, Text = "na" };
app.Add (lblBlue);
var lblDarker = new Label { X = Pos.AnchorEnd (32), Y = 8, Text = " " };
app.Add (lblDarker);
var lblLighter = new Label { X = Pos.AnchorEnd (32), Y = 9, Text = " " };
app.Add (lblLighter);
Application.MouseEvent += (s, e) =>
{
if (e.View == null)
{
return;
}
if (e.Flags == MouseFlags.Button1Clicked)
{
Attribute normal = e.View.GetAttributeForRole (VisualRole.Normal);
lblLighter.SetScheme (new (e.View.GetScheme ())
{
Normal = new (
normal.Foreground,
normal.Background.GetBrighterColor ()
)
});
}
else
{
Attribute normal = e.View.GetAttributeForRole (VisualRole.Normal);
lblRed.Text = normal.Foreground.R.ToString ();
lblGreen.Text = normal.Foreground.G.ToString ();
lblBlue.Text = normal.Foreground.B.ToString ();
}
};
Application.Run (app);
app.Dispose ();
Application.Shutdown ();
return;
void SetupGradient (string name, int x, ref int y, Func<int, Color> colorFunc)
{
var gradient = new Label { X = x, Y = y++, Text = name };
app.Add (gradient);
for (int dx = x, i = 0; i <= 256; i += 4)
{
var l = new Label
{
X = dx++,
Y = y
};
l.SetScheme (new ()
{
Normal = new (
colorFunc (Math.Clamp (i, 0, 255)),
colorFunc (Math.Clamp (i, 0, 255))
)
});
app.Add (l);
}
y += 2;
}
}
}