Selaa lähdekoodia

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.
Tig 3 viikkoa sitten
vanhempi
sitoutus
726b15dd28

+ 28 - 1
Examples/UICatalog/Scenarios/ColorPicker.cs

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

+ 0 - 157
Examples/UICatalog/Scenarios/TrueColors.cs

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

+ 0 - 6
Terminal.Gui/App/ApplicationImpl.Lifecycle.cs

@@ -145,13 +145,9 @@ public partial class ApplicationImpl
     }
 #endif
 
-    private bool _isResetingState;
-
     /// <inheritdoc/>
     public void ResetState (bool ignoreDisposed = false)
     {
-        _isResetingState = true;
-
         // Shutdown is the bookend for Init. As such it needs to clean up all resources
         // Init created. Apps that do any threading will need to code defensively for this.
         // e.g. see Issue #537
@@ -250,8 +246,6 @@ public partial class ApplicationImpl
         // gui.cs does no longer process any callbacks. See #1084 for more details:
         // (https://github.com/gui-cs/Terminal.Gui/issues/1084).
         SynchronizationContext.SetSynchronizationContext (null);
-
-        _isResetingState = false;
     }
 
     /// <summary>

+ 1 - 1
Terminal.Gui/Views/Color/ColorBar.cs

@@ -15,7 +15,7 @@ internal abstract class ColorBar : View, IColorBar
     /// </summary>
     protected ColorBar ()
     {
-        Height = 1;
+        Height = Dim.Auto(minimumContentDim: 1);
         Width = Dim.Fill ();
         CanFocus = true;
 

+ 3 - 2
Terminal.Gui/Views/Color/HueBar.cs

@@ -1,10 +1,11 @@
-
-
 using ColorHelper;
 using ColorConverter = ColorHelper.ColorConverter;
 
 namespace Terminal.Gui.Views;
 
+/// <summary>
+///     A bar representing the Hue component of a <see cref="Color"/> in HSL color space.
+/// </summary>
 internal class HueBar : ColorBar
 {
     /// <inheritdoc/>

+ 3 - 1
Tests/IntegrationTests/UICatalog/ScenarioTests.cs

@@ -35,7 +35,9 @@ public class ScenarioTests : TestsAllViews
             return;
         }
 
-        Application.ResetState (true);
+        // Force a complete reset
+        ApplicationImpl.SetInstance (null);
+        CM.Disable (true);
 
         _output.WriteLine ($"Running Scenario '{scenarioType}'");
         Scenario? scenario = null;

+ 0 - 0
Tests/UnitTestsParallelizable/Drivers/NetInputProcessorTests.cs → Tests/UnitTestsParallelizable/Drivers/Dotnet/NetInputProcessorTests.cs


+ 0 - 0
Tests/UnitTestsParallelizable/Drivers/WindowSizeMonitorTests.cs → Tests/UnitTestsParallelizable/Drivers/Windows/WindowSizeMonitorTests.cs


+ 0 - 0
Tests/UnitTestsParallelizable/Drivers/WindowsInputProcessorTests.cs → Tests/UnitTestsParallelizable/Drivers/Windows/WindowsInputProcessorTests.cs


+ 798 - 0
Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs

@@ -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
+}

+ 15 - 15
Tests/UnitTestsParallelizable/Views/ListViewTests.cs

@@ -893,7 +893,7 @@ public class ListViewTests (ITestOutputHelper output)
             BorderStyle = LineStyle.Single
         };
         lv.SetSource (["One", "Two", "Three", "Four"]);
-        lv.SelectedItemChanged += (s, e) => selected = e.Value.ToString ();
+        lv.SelectedItemChanged += (s, e) => selected = e.Value!.ToString ();
         var top = new Toplevel ();
         top.Add (lv);
         app.Begin (top);
@@ -950,7 +950,7 @@ public class ListViewTests (ITestOutputHelper output)
         Assert.Equal (2, lv.SelectedItem);
         top.Dispose ();
 
-        app.Shutdown ();
+        app?.Shutdown ();
     }
 
     [Fact]
@@ -1258,7 +1258,7 @@ Item 6",
         IApplication? app = Application.Create ();
         app.Init ("fake");
         IDriver? driver = app.Driver;
-        driver.SetScreenSize (8, 2);
+        driver?.SetScreenSize (8, 2);
 
         ObservableCollection<string> source = ["First", "Second"];
         var lv = new ListView { Width = Dim.Fill (), Height = 1, Source = new ListWrapper<string> (source) };
@@ -1282,7 +1282,7 @@ Item 6",
 
             for (var i = 0; i < 7; i++)
             {
-                item += app.Driver?.Contents [line, i].Rune;
+                item += (app.Driver?.Contents!) [line, i].Rune;
             }
 
             return item;
@@ -1430,8 +1430,8 @@ Three",
                                                        _output, app?.Driver);
 
         // Scroll down
-        app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledDown });
-        app.LayoutAndDraw ();
+        app?.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledDown });
+        app?.LayoutAndDraw ();
         Assert.Equal (1, lv.TopItem);
         DriverAssert.AssertDriverContentsWithFrameAre (
                                                        @"
@@ -1441,8 +1441,8 @@ Four ",
                                                        _output, app?.Driver);
 
         // Scroll up
-        app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledUp });
-        app.LayoutAndDraw ();
+        app?.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledUp });
+        app?.LayoutAndDraw ();
         Assert.Equal (0, lv.TopItem);
         DriverAssert.AssertDriverContentsWithFrameAre (
                                                        @"
@@ -1452,7 +1452,7 @@ Three",
                                                        _output, app?.Driver);
 
         top.Dispose ();
-        app.Shutdown ();
+        app?.Shutdown ();
     }
 
     [Fact]
@@ -1492,7 +1492,7 @@ Three - lo",
                                                        _output, app?.Driver);
 
         lv.ScrollHorizontal (1);
-        app.LayoutAndDraw ();
+        app?.LayoutAndDraw ();
         Assert.Equal (1, lv.LeftItem);
         DriverAssert.AssertDriverContentsWithFrameAre (
                                                        @"
@@ -1502,8 +1502,8 @@ hree - lon",
                                                        _output, app?.Driver);
 
         // Scroll right with mouse
-        app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledRight });
-        app.LayoutAndDraw ();
+        app?.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledRight });
+        app?.LayoutAndDraw ();
         Assert.Equal (2, lv.LeftItem);
         DriverAssert.AssertDriverContentsWithFrameAre (
                                                        @"
@@ -1513,8 +1513,8 @@ ree - long",
                                                        _output, app?.Driver);
 
         // Scroll left with mouse
-        app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledLeft });
-        app.LayoutAndDraw ();
+        app?.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.WheeledLeft });
+        app?.LayoutAndDraw ();
         Assert.Equal (1, lv.LeftItem);
         DriverAssert.AssertDriverContentsWithFrameAre (
                                                        @"
@@ -1524,7 +1524,7 @@ hree - lon",
                                                        _output, app?.Driver);
 
         top.Dispose ();
-        app.Shutdown ();
+        app?.Shutdown ();
     }
 
     [Fact]