#nullable enable using System.Collections.Concurrent; using Xunit.Abstractions; namespace DriverTests; /// /// Parallelizable unit tests for IInputProcessor.EnqueueMouseEvent. /// Tests validate the entire pipeline: MouseEventArgs → TInputRecord → Queue → ProcessQueue → Events. /// fully implemented in InputProcessorImpl (base class). Only WindowsInputProcessor has a working implementation. /// [Trait ("Category", "Input")] public class EnqueueMouseEventTests (ITestOutputHelper output) { private readonly ITestOutputHelper _output = output; #region Mouse Event Sequencing Tests [Fact] public void FakeInput_EnqueueMouseEvent_HandlesCompleteClickSequence () { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; List receivedEvents = []; processor.MouseEvent += (_, e) => receivedEvents.Add (e); // Act - Simulate a complete click: press → release → click processor.EnqueueMouseEvent ( null, new () { Position = new (10, 5), Flags = MouseFlags.Button1Pressed }); processor.EnqueueMouseEvent ( null, new () { Position = new (10, 5), Flags = MouseFlags.Button1Released }); // The MouseInterpreter in the processor should generate a clicked event SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert // We should see at least the pressed and released events Assert.True (receivedEvents.Count >= 2); Assert.Contains (receivedEvents, e => e.Flags.HasFlag (MouseFlags.Button1Pressed)); Assert.Contains (receivedEvents, e => e.Flags.HasFlag (MouseFlags.Button1Released)); } #endregion #region Thread Safety Tests [Fact] public void FakeInput_EnqueueMouseEvent_IsThreadSafe () { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; ConcurrentBag receivedEvents = []; processor.MouseEvent += (_, e) => receivedEvents.Add (e); const int threadCount = 10; const int eventsPerThread = 100; Thread [] threads = new Thread [threadCount]; // Act - Enqueue mouse events from multiple threads for (var t = 0; t < threadCount; t++) { int threadId = t; threads [t] = new (() => { for (var i = 0; i < eventsPerThread; i++) { processor.EnqueueMouseEvent ( null, new () { Position = new (threadId, i), Flags = MouseFlags.Button1Clicked }); } }); threads [t].Start (); } // Wait for all threads to complete foreach (Thread thread in threads) { thread.Join (); } SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert Assert.Equal (threadCount * eventsPerThread, receivedEvents.Count); } #endregion #region Helper Methods /// /// Simulates the input thread by manually draining FakeInput's internal queue /// and moving items to the InputBuffer. This is needed because tests don't /// start the actual input thread via Run(). /// private static void SimulateInputThread (FakeInput fakeInput, ConcurrentQueue inputBuffer) { // FakeInput's Peek() checks _testInput while (fakeInput.Peek ()) { // Read() drains _testInput and returns items foreach (ConsoleKeyInfo item in fakeInput.Read ()) { // Manually add to InputBuffer (simulating what Run() would do) inputBuffer.Enqueue (item); } } } #endregion #region FakeInputProcessor EnqueueMouseEvent Tests [Fact] public void FakeInput_EnqueueMouseEvent_AddsSingleMouseEventToQueue () { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; List receivedEvents = []; processor.MouseEvent += (_, e) => receivedEvents.Add (e); MouseEventArgs mouseEvent = new () { Position = new (10, 5), Flags = MouseFlags.Button1Clicked }; // Act processor.EnqueueMouseEvent (null, mouseEvent); SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert - Verify the mouse event made it through Assert.Single (receivedEvents); Assert.Equal (mouseEvent.Position, receivedEvents [0].Position); Assert.Equal (mouseEvent.Flags, receivedEvents [0].Flags); } [Fact] public void FakeInput_EnqueueMouseEvent_SupportsMultipleEvents () { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; MouseEventArgs [] events = [ new () { Position = new (10, 5), Flags = MouseFlags.Button1Pressed }, new () { Position = new (10, 5), Flags = MouseFlags.Button1Released }, new () { Position = new (15, 8), Flags = MouseFlags.ReportMousePosition }, new () { Position = new (20, 10), Flags = MouseFlags.Button1Clicked } ]; List receivedEvents = []; processor.MouseEvent += (_, e) => receivedEvents.Add (e); // Act foreach (MouseEventArgs mouseEvent in events) { processor.EnqueueMouseEvent (null, mouseEvent); } SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert // The MouseInterpreter processes Button1Pressed followed by Button1Released and generates // an additional Button1Clicked event, so we expect 5 events total: // 1. Button1Pressed (original) // 2. Button1Released (original) // 3. Button1Clicked (generated by MouseInterpreter from press+release) // 4. ReportMousePosition (original) // 5. Button1Clicked (original) Assert.Equal (5, receivedEvents.Count); // Verify the original events are present Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.Button1Pressed && e.Position == new Point (10, 5)); Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.Button1Released && e.Position == new Point (10, 5)); Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.ReportMousePosition && e.Position == new Point (15, 8)); // There should be two clicked events: one generated, one original List clickedEvents = receivedEvents.Where (e => e.Flags == MouseFlags.Button1Clicked).ToList (); Assert.Equal (2, clickedEvents.Count); Assert.Contains (clickedEvents, e => e.Position == new Point (10, 5)); // Generated from press+release Assert.Contains (clickedEvents, e => e.Position == new Point (20, 10)); // Original } [Theory] [InlineData (MouseFlags.Button1Clicked)] [InlineData (MouseFlags.Button2Clicked)] [InlineData (MouseFlags.Button3Clicked)] [InlineData (MouseFlags.Button4Clicked)] [InlineData (MouseFlags.Button1DoubleClicked)] [InlineData (MouseFlags.Button1TripleClicked)] public void FakeInput_EnqueueMouseEvent_SupportsAllButtonClicks (MouseFlags flags) { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; MouseEventArgs mouseEvent = new () { Position = new (10, 5), Flags = flags }; MouseEventArgs? receivedEvent = null; processor.MouseEvent += (_, e) => receivedEvent = e; // Act processor.EnqueueMouseEvent (null, mouseEvent); SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert Assert.NotNull (receivedEvent); Assert.Equal (flags, receivedEvent.Flags); } [Theory] [InlineData (0, 0)] [InlineData (10, 5)] [InlineData (79, 24)] // Near screen edge (assuming 80x25) [InlineData (100, 100)] // Beyond typical screen public void FakeInput_EnqueueMouseEvent_PreservesPosition (int x, int y) { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; MouseEventArgs mouseEvent = new () { Position = new (x, y), Flags = MouseFlags.Button1Clicked }; MouseEventArgs? receivedEvent = null; processor.MouseEvent += (_, e) => receivedEvent = e; // Act processor.EnqueueMouseEvent (null, mouseEvent); SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert Assert.NotNull (receivedEvent); Assert.Equal (x, receivedEvent.Position.X); Assert.Equal (y, receivedEvent.Position.Y); } [Theory] [InlineData (MouseFlags.ButtonShift)] [InlineData (MouseFlags.ButtonCtrl)] [InlineData (MouseFlags.ButtonAlt)] [InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonCtrl)] [InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonAlt)] [InlineData (MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt)] [InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt)] public void FakeInput_EnqueueMouseEvent_PreservesModifiers (MouseFlags modifiers) { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; MouseEventArgs mouseEvent = new () { Position = new (10, 5), Flags = MouseFlags.Button1Clicked | modifiers }; MouseEventArgs? receivedEvent = null; processor.MouseEvent += (_, e) => receivedEvent = e; // Act processor.EnqueueMouseEvent (null, mouseEvent); SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert Assert.NotNull (receivedEvent); Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.Button1Clicked)); if (modifiers.HasFlag (MouseFlags.ButtonShift)) { Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonShift)); } if (modifiers.HasFlag (MouseFlags.ButtonCtrl)) { Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonCtrl)); } if (modifiers.HasFlag (MouseFlags.ButtonAlt)) { Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonAlt)); } } [Theory] [InlineData (MouseFlags.WheeledUp)] [InlineData (MouseFlags.WheeledDown)] [InlineData (MouseFlags.WheeledLeft)] [InlineData (MouseFlags.WheeledRight)] public void FakeInput_EnqueueMouseEvent_SupportsMouseWheel (MouseFlags wheelFlag) { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; MouseEventArgs mouseEvent = new () { Position = new (10, 5), Flags = wheelFlag }; MouseEventArgs? receivedEvent = null; processor.MouseEvent += (_, e) => receivedEvent = e; // Act processor.EnqueueMouseEvent (null, mouseEvent); SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert Assert.NotNull (receivedEvent); Assert.True (receivedEvent.Flags.HasFlag (wheelFlag)); } [Fact] public void FakeInput_EnqueueMouseEvent_SupportsMouseMove () { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; List receivedEvents = []; processor.MouseEvent += (_, e) => receivedEvents.Add (e); MouseEventArgs [] events = [ new () { Position = new (0, 0), Flags = MouseFlags.ReportMousePosition }, new () { Position = new (5, 5), Flags = MouseFlags.ReportMousePosition }, new () { Position = new (10, 10), Flags = MouseFlags.ReportMousePosition } ]; // Act foreach (MouseEventArgs mouseEvent in events) { processor.EnqueueMouseEvent (null, mouseEvent); } SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert Assert.Equal (3, receivedEvents.Count); Assert.Equal (new (0, 0), receivedEvents [0].Position); Assert.Equal (new (5, 5), receivedEvents [1].Position); Assert.Equal (new (10, 10), receivedEvents [2].Position); } #endregion #region InputProcessor Pipeline Tests [Fact] public void InputProcessor_EnqueueMouseEvent_DoesNotThrow () { // Arrange ConcurrentQueue queue = new (); var processor = new FakeInputProcessor (queue); // Don't set InputImpl (or set to non-testable) // Act & Assert - Should not throw even if not implemented Exception? exception = Record.Exception (() => { processor.EnqueueMouseEvent ( null, new () { Position = new (10, 5), Flags = MouseFlags.Button1Clicked }); processor.ProcessQueue (); }); // The base implementation logs a critical message but doesn't throw Assert.Null (exception); } [Fact] public void InputProcessor_ProcessQueue_DrainsPendingMouseEvents () { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; List receivedEvents = []; processor.MouseEvent += (_, e) => receivedEvents.Add (e); // Act - Enqueue multiple events before processing processor.EnqueueMouseEvent (null, new () { Position = new (1, 1), Flags = MouseFlags.Button1Pressed }); processor.EnqueueMouseEvent (null, new () { Position = new (2, 2), Flags = MouseFlags.ReportMousePosition }); processor.EnqueueMouseEvent (null, new () { Position = new (3, 3), Flags = MouseFlags.Button1Released }); SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); // Assert - After processing, all events should be received Assert.Empty (queue); Assert.Equal (3, receivedEvents.Count); } #endregion #region Error Handling Tests [Fact] public void FakeInput_EnqueueMouseEvent_WithInvalidEvent_DoesNotThrow () { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; // Act & Assert - Empty/default mouse event should not throw Exception? exception = Record.Exception (() => { processor.EnqueueMouseEvent (null, new ()); SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); }); Assert.Null (exception); } [Fact] public void FakeInput_EnqueueMouseEvent_WithNegativePosition_DoesNotThrow () { // Arrange var fakeInput = new FakeInput (); ConcurrentQueue queue = new (); fakeInput.Initialize (queue); var processor = new FakeInputProcessor (queue); processor.InputImpl = fakeInput; // Act & Assert - Negative positions should not throw Exception? exception = Record.Exception (() => { processor.EnqueueMouseEvent ( null, new () { Position = new (-10, -5), Flags = MouseFlags.Button1Clicked }); SimulateInputThread (fakeInput, queue); processor.ProcessQueue (); }); Assert.Null (exception); } #endregion }