Input Injection
Quick Start: Most developers only need to use
app.InjectKey()andapp.InjectMouse(). See Simple Examples below.
Overview
Input injection allows tests to simulate user input (keyboard and mouse) without requiring actual hardware. The system provides:
- Single-call API -
app.InjectKey(Key.A)handles everything - Virtual time control - Tests run instantly, no real delays
- Two modes - Direct (default, fast) and Pipeline (full ANSI encoding/parsing)
- Deterministic behavior - Same input → same result, every time
Simple Examples
Basic Keyboard Input
using IApplication app = Application.Create();
app.Init(DriverRegistry.Names.ANSI);
// Subscribe to key events
app.Keyboard.KeyDown += (s, e) => Console.WriteLine($"Key: {e}");
// Inject keys
app.InjectKey(Key.A);
app.InjectKey(Key.Enter);
app.InjectKey(Key.Esc);
Basic Mouse Input
using IApplication app = Application.Create();
app.Init(DriverRegistry.Names.ANSI);
// Subscribe to mouse events
app.Mouse.MouseEvent += (s, e) => Console.WriteLine($"Mouse: {e.Flags} at {e.ScreenPosition}");
// Inject mouse click
app.InjectMouse(new() {
ScreenPosition = new(10, 5),
Flags = MouseFlags.LeftButtonPressed
});
app.InjectMouse(new() {
ScreenPosition = new(10, 5),
Flags = MouseFlags.LeftButtonReleased
});
Testing with Virtual Time
// Create virtual time provider for deterministic timing
VirtualTimeProvider time = new();
time.SetTime(new DateTime(2025, 1, 1, 12, 0, 0));
using IApplication app = Application.Create(time);
app.Init(DriverRegistry.Names.ANSI);
// First click at T+0
app.InjectMouse(new() {
ScreenPosition = new(10, 5),
Flags = MouseFlags.LeftButtonPressed,
Timestamp = time.Now
});
time.Advance(TimeSpan.FromMilliseconds(50));
app.InjectMouse(new() {
ScreenPosition = new(10, 5),
Flags = MouseFlags.LeftButtonReleased,
Timestamp = time.Now
});
// Second click at T+300 (within double-click threshold)
time.Advance(TimeSpan.FromMilliseconds(250));
app.InjectMouse(new() {
ScreenPosition = new(10, 5),
Flags = MouseFlags.LeftButtonPressed,
Timestamp = time.Now
});
time.Advance(TimeSpan.FromMilliseconds(50));
app.InjectMouse(new() {
ScreenPosition = new(10, 5),
Flags = MouseFlags.LeftButtonReleased,
Timestamp = time.Now
});
// Double-click was detected!
When to Use Each Mode
Direct Mode (Default)
Use for: 99% of tests - view behavior, command execution, event handling
// Direct mode is default - just inject
app.InjectKey(Key.A);
app.InjectMouse(new() { ScreenPosition = new(10, 5), Flags = MouseFlags.LeftButtonClicked });
Pipeline Mode (ANSI Testing)
Use for: Testing ANSI encoding/parsing, escape sequences, driver behavior
// Explicit Pipeline mode
IInputInjector injector = app.GetInputInjector();
InputInjectionOptions options = new() { Mode = InputInjectionMode.Pipeline };
// This tests: Key.F1 → "\x1b[OP" → Parser → Key.F1
injector.InjectKey(Key.F1, options);
Auto Mode
Purpose: Let the system choose the appropriate mode
Behavior: Currently defaults to Direct mode for performance
When to Use: When there are no specific requirements for mode selection
Detailed Documentation
The sections below provide in-depth documentation for advanced scenarios.
- Virtual Time Details
- Injection Modes Deep Dive
- Architecture Layers
- Testing Patterns
- Best Practices
- Advanced Topics
- Troubleshooting
Virtual Time Details
Virtual time is the foundation of deterministic testing. Instead of relying on DateTime.Now and real delays, tests explicitly control time advancement.
How Virtual Time Works
// Create virtual time provider
VirtualTimeProvider time = new();
// Set initial time
time.SetTime(new DateTime(2025, 1, 1, 12, 0, 0));
// Advance time by 100ms (instant - no real delay)
time.Advance(TimeSpan.FromMilliseconds(100));
// Current virtual time is now 12:00:00.100
DateTime now = time.Now; // 2025-01-01 12:00:00.100
Benefits of Virtual Time
- Fast - No real delays;
time.Advance(TimeSpan.FromSeconds(10))is instant - Precise - Control timing to the millisecond
- Repeatable - Same time sequence → same test results
- Debuggable - Pause time, inspect state, advance step-by-step
Components Using Virtual Time
All timing-dependent components accept ITimeProvider:
MouseButtonClickTracker- Double/triple-click detection based on time thresholdsMouseInterpreter- Click timing and multi-click synthesisAnsiResponseParser- Escape sequence timeout detection (50ms)- Application timers and delays - All time-based operations
Injection Modes Deep Dive
Input Injection Modes
The system supports two injection modes to balance speed and coverage:
Direct Mode (Default)
Purpose: Fast, simple testing of view/application logic
Flow:
InputInjector → InputProcessor → Events
Characteristics:
- Bypasses ANSI encoding/decoding
- Preserves all event properties (timestamps, flags, etc.)
- Fastest execution
- Default for most tests
When to Use:
- Testing view behavior (button clicks, text input)
- Testing mouse event handling
- Testing command execution
- Any test not specifically testing ANSI encoding
Example:
// Direct mode (default)
VirtualTimeProvider time = new();
using IApplication app = Application.Create(time);
app.Init(DriverRegistry.Names.ANSI);
// Injection goes directly to events
app.InjectKey(Key.Enter); // Fast, bypasses ANSI encoding
Pipeline Mode
Purpose: Full ANSI encoding/parsing pipeline testing
Flow:
InputInjector → ANSI Encoder → TestInputSource (chars) → AnsiResponseParser → InputProcessor → Events
Characteristics:
- Tests full ANSI escape sequence encoding
- Tests ANSI parser behavior
- Validates round-trip encoding/decoding
- Slightly slower (more processing steps)
When to Use:
- Testing ANSI keyboard encoding (e.g.,
Key.F1→"\x1b[OP") - Testing ANSI mouse encoding (e.g., SGR format
"\x1b[<0;10;5M") - Testing escape sequence parsing
- Testing parser timeout behavior
Example:
// Pipeline mode for ANSI testing
VirtualTimeProvider time = new();
using IApplication app = Application.Create(time);
app.Init(DriverRegistry.Names.ANSI);
InputInjectionOptions options = new()
{
Mode = InputInjectionMode.Pipeline
};
// This encodes Key.F1 → "\x1b[OP", injects chars, parses back
app.GetInputInjector().InjectKey(Key.F1, options);
// Verify ANSI encoding worked correctly
// (parser should decode "\x1b[OP" back to Key.F1)
Auto Mode
Purpose: Let the system choose the appropriate mode
Behavior: Currently defaults to Direct mode for performance
When to Use: When there are no specific requirements for mode selection
Architecture Layers
Layer 1: Time Abstraction
Location: Terminal.Gui/Time/ITimeProvider.cs
Purpose: Provide pluggable time source for testing and production
ITimeProvider Interface
/// <summary>
/// Abstraction for time-related operations, allowing virtual time in tests.
/// </summary>
public interface ITimeProvider
{
/// <summary>
/// Gets the current date/time. In tests, this can be controlled.
/// </summary>
DateTime Now { get; }
/// <summary>
/// Creates a delay. In tests, this can be instant or controlled.
/// </summary>
Task Delay(TimeSpan duration, CancellationToken cancellationToken = default);
/// <summary>
/// Creates a timer. In tests, this can be controlled.
/// </summary>
ITimer CreateTimer(TimeSpan interval, Action callback);
}
SystemTimeProvider (Production)
/// <summary>
/// Real time provider using DateTime.Now and Task.Delay.
/// </summary>
public class SystemTimeProvider : ITimeProvider
{
public DateTime Now => DateTime.Now;
public Task Delay(TimeSpan duration, CancellationToken ct)
=> Task.Delay(duration, ct);
public ITimer CreateTimer(TimeSpan interval, Action callback)
=> new SystemTimer(interval, callback);
}
Usage: Production applications use system time automatically
VirtualTimeProvider (Testing)
/// <summary>
/// Virtual time provider for testing - all time is controlled.
/// </summary>
public class VirtualTimeProvider : ITimeProvider
{
private DateTime _currentTime = new (2025, 1, 1, 0, 0, 0);
public DateTime Now => _currentTime;
/// <summary>
/// Advance virtual time by the specified duration.
/// </summary>
public void Advance(TimeSpan duration)
{
_currentTime += duration;
// Trigger timers and complete delays
}
/// <summary>
/// Set virtual time to a specific value.
/// </summary>
public void SetTime(DateTime time)
{
_currentTime = time;
}
}
Usage: Tests create and control virtual time explicitly
Key Design Decision: All timing-dependent code uses ITimeProvider instead of DateTime.Now directly. This single change enables complete time control in tests.
Layer 2: Input Source
Location: Terminal.Gui/Drivers/Input/IInputSource.cs
Purpose: Provide input records to the input processor
IInputSource Interface
/// <summary>
/// Source of input events. Production implementations read from console,
/// test implementations provide pre-programmed input.
/// </summary>
public interface IInputSource
{
/// <summary>
/// Time provider for timestamps and timing.
/// </summary>
ITimeProvider TimeProvider { get; }
/// <summary>
/// Check if input is available without consuming it.
/// </summary>
bool IsAvailable { get; }
/// <summary>
/// Read all available input synchronously.
/// </summary>
IEnumerable<InputRecord> ReadAvailable();
/// <summary>
/// Start background input reading (for production).
/// </summary>
void Start(CancellationToken cancellationToken);
/// <summary>
/// Stop background input reading.
/// </summary>
void Stop();
}
Input Record Types
All input flows through platform-independent records:
/// <summary>
/// Base class for input records.
/// </summary>
public abstract record InputRecord
{
/// <summary>
/// When this input occurred (set by ITimeProvider).
/// </summary>
public DateTime Timestamp { get; init; }
}
/// <summary>
/// Keyboard input record.
/// </summary>
public record KeyboardRecord(Key Key) : InputRecord;
/// <summary>
/// Mouse input record (raw, before click synthesis).
/// </summary>
public record MouseRecord(Mouse Mouse) : InputRecord;
/// <summary>
/// ANSI sequence record (for Pipeline mode testing).
/// </summary>
public record AnsiRecord(string Sequence) : InputRecord;
TestInputSource (Testing)
/// <summary>
/// Test input source - provides pre-programmed input via queue.
/// </summary>
public class TestInputSource : IInputSource
{
private readonly Queue<InputRecord> _inputQueue = new ();
public ITimeProvider TimeProvider { get; }
public bool IsAvailable => _inputQueue.Count > 0;
public IEnumerable<InputRecord> ReadAvailable()
{
while (_inputQueue.Count > 0)
{
yield return _inputQueue.Dequeue();
}
}
/// <summary>
/// Add input to the queue (called by InputInjector).
/// </summary>
public void Enqueue(InputRecord record)
{
// Stamp with current virtual time if not set
if (record.Timestamp == default)
{
record = record with { Timestamp = TimeProvider.Now };
}
_inputQueue.Enqueue(record);
}
}
Key Characteristics:
- Thread-safe queue
- Automatic timestamping via
ITimeProvider - No background thread (synchronous testing)
- Complete control over input sequence
ConsoleInputSource (Production)
/// <summary>
/// Console input source - reads from actual console.
/// </summary>
public abstract class ConsoleInputSource : IInputSource
{
protected readonly ConcurrentQueue<InputRecord> InputBuffer = new ();
public void Start(CancellationToken cancellationToken)
{
// Start background thread that reads from console
Task.Run(() => ReadLoop(cancellationToken), cancellationToken);
}
/// <summary>
/// Platform-specific console reading (overridden by drivers).
/// </summary>
protected abstract Task ReadLoop(CancellationToken cancellationToken);
}
Platform Implementations:
WindowsInputSource- ReadsInputRecordfrom Windows Console APINetInputSource- ReadsConsoleKeyInfofrom .NET ConsoleUnixInputSource- Readscharfrom Unix terminalAnsiInputSource- Readscharand parses ANSI sequences
Layer 3: Input Processor
Location: Terminal.Gui/Drivers/InputProcessor/InputProcessor.cs
Purpose: Convert input records to Terminal.Gui events
IInputProcessor Interface
/// <summary>
/// Input processor - consumes input from IInputSource and raises events.
/// </summary>
public interface IInputProcessor
{
/// <summary>
/// Input source providing input records.
/// </summary>
IInputSource InputSource { get; }
/// <summary>
/// Time provider for timing-dependent operations.
/// </summary>
ITimeProvider TimeProvider { get; }
/// <summary>
/// Process all available input from the source.
/// </summary>
void ProcessInput();
/// <summary>
/// Keyboard events.
/// </summary>
event EventHandler<Key>? KeyDown;
event EventHandler<Key>? KeyUp;
/// <summary>
/// Mouse event (includes synthesized clicks).
/// </summary>
event EventHandler<Mouse>? MouseEvent;
/// <summary>
/// ANSI sequence parser (for ANSI drivers only).
/// </summary>
IAnsiResponseParser? AnsiParser { get; }
}
InputProcessor Implementation
/// <summary>
/// Implementation of IInputProcessor.
/// </summary>
public class InputProcessor : IInputProcessor
{
private readonly MouseInterpreter _mouseInterpreter;
private readonly IAnsiResponseParser? _ansiParser;
public IInputSource InputSource { get; }
public ITimeProvider TimeProvider { get; }
public void ProcessInput()
{
// 1. Read all available input
foreach (InputRecord record in InputSource.ReadAvailable())
{
ProcessRecord(record);
}
// 2. Check for stale escape sequences
if (_ansiParser?.IsEscapeSequenceStale() == true)
{
// Release held Esc key via virtual time
foreach (char released in _ansiParser.Release())
{
ProcessRecord(new AnsiRecord(released.ToString())
{
Timestamp = TimeProvider.Now
});
}
}
}
private void ProcessRecord(InputRecord record)
{
switch (record)
{
case KeyboardRecord kr:
RaiseKeyboard(kr.Key);
break;
case MouseRecord mr:
RaiseMouse(mr.Mouse);
break;
case AnsiRecord ar:
// Feed to ANSI parser (Pipeline mode)
_ansiParser?.ProcessInput(ar.Sequence);
break;
}
}
private void RaiseMouse(Mouse mouse)
{
// Process through MouseInterpreter for click synthesis
foreach (Mouse processedMouse in _mouseInterpreter.Process(mouse))
{
MouseEvent?.Invoke(this, processedMouse);
}
}
}
Key Features:
- Single
ProcessInput()call processes all queued input - Automatic escape sequence timeout via
ITimeProvider - Optional ANSI parsing for Pipeline mode
- Click synthesis via
MouseInterpreter
Layer 4: Input Injector
Location: Terminal.Gui/Testing/InputInjector.cs
Purpose: High-level API for test input injection
IInputInjector Interface
/// <summary>
/// High-level input injection API - single entry point for all injection.
/// </summary>
public interface IInputInjector
{
/// <summary>
/// Inject a keyboard event.
/// </summary>
void InjectKey(Key key, InputInjectionOptions? options = null);
/// <summary>
/// Inject a mouse event.
/// </summary>
void InjectMouse(Mouse mouse, InputInjectionOptions? options = null);
/// <summary>
/// Inject a sequence of input events with delays.
/// </summary>
void InjectSequence(IEnumerable<InputEvent> events, InputInjectionOptions? options = null);
/// <summary>
/// Force processing of the input queue (usually automatic).
/// </summary>
void ProcessQueue();
}
Input Injection Options
/// <summary>
/// Configuration for input injection behavior.
/// </summary>
public class InputInjectionOptions
{
/// <summary>
/// Injection mode (Direct, Pipeline, or Auto).
/// </summary>
public InputInjectionMode Mode { get; set; } = InputInjectionMode.Auto;
/// <summary>
/// Whether to automatically process the input queue after injection.
/// </summary>
public bool AutoProcess { get; set; } = true;
/// <summary>
/// Time provider to use for timestamps and timing.
/// </summary>
public ITimeProvider? TimeProvider { get; set; }
}
InputInjector Implementation
/// <summary>
/// Implementation of IInputInjector for testing.
/// </summary>
public class InputInjector : IInputInjector
{
private readonly IInputProcessor _processor;
private readonly TestInputSource? _testSource;
private readonly ITimeProvider _timeProvider;
public void InjectKey(Key key, InputInjectionOptions? options = null)
{
options ??= new InputInjectionOptions();
InputInjectionMode mode = ResolveMode(options.Mode);
InputRecord record;
if (mode == InputInjectionMode.Direct)
{
// Direct: Bypass encoding
record = new KeyboardRecord(key)
{
Timestamp = _timeProvider.Now
};
}
else // Pipeline
{
// Encode to ANSI sequence
string ansiSequence = AnsiKeyboardEncoder.Encode(key);
record = new AnsiRecord(ansiSequence)
{
Timestamp = _timeProvider.Now
};
}
_testSource.Enqueue(record);
if (options.AutoProcess)
{
ProcessQueue();
}
}
public void ProcessQueue()
{
_processor.ProcessInput();
// If escape sequences are stale, advance time and process again
if (_timeProvider is VirtualTimeProvider vtp)
{
if (_processor.AnsiParser?.State == AnsiResponseParserState.ExpectingEscapeSequence)
{
vtp.Advance(TimeSpan.FromMilliseconds(60)); // Past 50ms timeout
_processor.ProcessInput();
}
}
}
}
Key Features:
- Mode selection (Direct vs Pipeline)
- Automatic processing by default
- Automatic escape sequence handling via virtual time
- Timestamp management
Layer 5: Application Integration
Location: Terminal.Gui/Testing/InputInjectionExtensions.cs
Purpose: Convenient extension methods on IApplication
Extension Methods
/// <summary>
/// Extension methods for input injection.
/// </summary>
public static class InputInjectionExtensions
{
private static readonly ConditionalWeakTable<IApplication, IInputInjector> _injectorCache = new ();
/// <summary>
/// Get or create the input injector for this application.
/// </summary>
public static IInputInjector GetInputInjector(this IApplication app)
{
return _injectorCache.GetValue(app, _ =>
{
ITimeProvider timeProvider = app.GetTimeProvider();
IInputProcessor processor = app.Driver.GetInputProcessor();
return new InputInjector(processor, timeProvider);
});
}
/// <summary>
/// Inject a key event (convenience method).
/// </summary>
public static void InjectKey(this IApplication app, Key key)
{
app.GetInputInjector().InjectKey(key);
}
/// <summary>
/// Inject a mouse event (convenience method).
/// </summary>
public static void InjectMouse(this IApplication app, Mouse mouse)
{
app.GetInputInjector().InjectMouse(mouse);
}
/// <summary>
/// Inject a sequence of events (convenience method).
/// </summary>
public static void InjectSequence(this IApplication app, params InputEvent[] events)
{
app.GetInputInjector().InjectSequence(events);
}
}
Key Features:
- Cached injector per application instance
- Clean API via extension methods
- Automatic setup and teardown
Testing Patterns
Simple Unit Tests
Goal: Test view behavior without timing concerns
// Simple button click test
[Fact]
public void Button_ClickWithMouse_RaisesAccepting()
{
VirtualTimeProvider time = new ();
using IApplication app = Application.Create(time);
app.Init(DriverRegistry.Names.ANSI);
Button button = new () { Text = "Click Me" };
bool acceptingCalled = false;
button.Accepting += (s, e) => acceptingCalled = true;
// Single-call injection
app.InjectMouse(new ()
{
Flags = MouseFlags.LeftButtonPressed,
Position = new (5, 5)
});
app.InjectMouse(new ()
{
Flags = MouseFlags.LeftButtonReleased,
Position = new (5, 5)
});
Assert.True(acceptingCalled);
}
Timing-Dependent Tests
Goal: Test double-click, timing thresholds, etc.
// Double-click detection test
[Fact]
public void DoubleClick_WithinThreshold_DetectsDoubleClick()
{
VirtualTimeProvider time = new ();
time.SetTime(new DateTime(2025, 1, 1, 12, 0, 0));
using IApplication app = Application.Create(time);
app.Init(DriverRegistry.Names.ANSI);
List<MouseFlags> receivedFlags = [];
app.Mouse.MouseEvent += (s, e) => receivedFlags.Add(e.Flags);
// First click at T+0
app.InjectMouse(new ()
{
Flags = MouseFlags.LeftButtonPressed,
Position = new (5, 5)
});
app.InjectMouse(new ()
{
Flags = MouseFlags.LeftButtonReleased,
Position = new (5, 5)
});
// Advance virtual time by 300ms
time.Advance(TimeSpan.FromMilliseconds(300));
// Second click at T+300 (within 500ms threshold = double-click)
app.InjectMouse(new ()
{
Flags = MouseFlags.LeftButtonPressed,
Position = new (5, 5)
});
app.InjectMouse(new ()
{
Flags = MouseFlags.LeftButtonReleased,
Position = new (5, 5)
});
// Verify double-click detected
Assert.Contains(receivedFlags, f => f.HasFlag(MouseFlags.LeftButtonDoubleClicked));
}
Key Points:
- Explicit time control via
time.Advance() - Precise timing to millisecond
- No real delays - tests run instantly
- Repeatable results every time
ANSI Pipeline Tests
Goal: Test ANSI encoding/parsing pipeline
// Test ANSI keyboard encoding
[Fact]
public void AnsiEncoding_F1Key_EncodesAndDecodesCorrectly()
{
VirtualTimeProvider time = new ();
using IApplication app = Application.Create(time);
app.Init(DriverRegistry.Names.ANSI);
Key? receivedKey = null;
app.Driver.KeyDown += (s, e) => receivedKey = e;
// Force Pipeline mode to test ANSI encoding
InputInjectionOptions options = new ()
{
Mode = InputInjectionMode.Pipeline
};
// This will:
// 1. Encode Key.F1 → "\x1b[OP"
// 2. Inject chars individually
// 3. Parser detects escape sequence
// 4. Decodes back to Key.F1
app.InjectKey(Key.F1, options);
Assert.Equal(Key.F1, receivedKey);
}
Key Points:
- Explicit Pipeline mode selection
- Tests full ANSI round-trip
- Validates encoding/decoding correctness
Integration Tests
Goal: Test complete application lifecycle with fluent API
// Integration test using fluent API
[Fact]
public void Application_FullLifecycle_WorksCorrectly()
{
using GuiTestContext context = With.A<Window>(40, 10, TestDriver.ANSI)
.Add(new Button { Text = "Click Me", X = 5, Y = 5 })
.LeftClick(6, 6) // Click button
.InjectKey(Key.Tab) // Navigate
.InjectKey(Key.Enter) // Activate
.AssertTrue(app => app.IsShuttingDown == false);
// Context automatically manages application lifecycle
}
Best Practices
1. Always Use Virtual Time for Tests
// ✅ CORRECT - Virtual time for determinism
VirtualTimeProvider time = new ();
using IApplication app = Application.Create(time);
// ❌ WRONG - System time causes flaky tests
using IApplication app = Application.Create(); // Uses SystemTimeProvider
2. Use ANSI Driver for Tests
// ✅ CORRECT - ANSI driver is cross-platform
app.Init(DriverRegistry.Names.ANSI);
// ⚠️ AVOID - Platform-specific drivers in tests
app.Init(DriverRegistry.Names.WINDOWS); // Won't run on Unix
3. Use Direct Mode by Default
// ✅ CORRECT - Default Direct mode is fast
app.InjectKey(Key.Enter);
// ⚠️ ONLY when testing ANSI encoding
app.InjectKey(Key.F1, new () { Mode = InputInjectionMode.Pipeline });
4. Advance Time Explicitly
// ✅ CORRECT - Explicit time advancement
app.InjectMouse(firstClick);
time.Advance(TimeSpan.FromMilliseconds(50));
app.InjectMouse(secondClick);
// ❌ WRONG - Real delays defeat virtual time
app.InjectMouse(firstClick);
Thread.Sleep(300); // Uses real time!
app.InjectMouse(secondClick);
5. Use Input Sequences for Complex Scenarios
// ✅ CORRECT - Declarative sequence with delays
app.InjectSequence(
new KeyEvent(Key.H),
new KeyEvent(Key.E) { Delay = TimeSpan.FromMilliseconds(100) },
new KeyEvent(Key.L),
new KeyEvent(Key.L),
new KeyEvent(Key.O)
);
// ⚠️ VERBOSE - Manual injection
app.InjectKey(Key.H);
time.Advance(TimeSpan.FromMilliseconds(100));
app.InjectKey(Key.E);
// ... etc
Advanced Topics
Custom Input Sources
You can create custom input sources for specialized testing:
// Custom input source for file replay
public class FileInputSource : IInputSource
{
private readonly Queue<InputRecord> _recordedInput;
public FileInputSource(string recordingFile, ITimeProvider timeProvider)
{
TimeProvider = timeProvider;
_recordedInput = LoadRecording(recordingFile);
}
public IEnumerable<InputRecord> ReadAvailable()
{
while (_recordedInput.Count > 0)
{
yield return _recordedInput.Dequeue();
}
}
}
Custom Time Providers
Advanced scenarios may need custom time behavior:
// Time provider with variable speed
public class ScaledTimeProvider : ITimeProvider
{
private readonly double _timeScale; // 2.0 = 2x speed
private DateTime _baseTime = DateTime.Now;
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
public DateTime Now => _baseTime + TimeSpan.FromTicks(
(long)(_stopwatch.Elapsed.Ticks * _timeScale)
);
}
Input Sequences
Complex input patterns can be defined declaratively:
// Define reusable input sequences
public static class InputSequences
{
public static InputEvent[] TypeText(string text)
{
return text.Select(c => new KeyEvent(Key.Create(c))).ToArray();
}
public static InputEvent[] DoubleClick(Point position)
{
return
[
new MouseEvent(new () { Flags = MouseFlags.LeftButtonPressed, Position = position }),
new MouseEvent(new () { Flags = MouseFlags.LeftButtonReleased, Position = position })
{ Delay = TimeSpan.FromMilliseconds(50) },
new MouseEvent(new () { Flags = MouseFlags.LeftButtonPressed, Position = position })
{ Delay = TimeSpan.FromMilliseconds(300) },
new MouseEvent(new () { Flags = MouseFlags.LeftButtonReleased, Position = position })
{ Delay = TimeSpan.FromMilliseconds(50) }
];
}
}
// Use in tests
app.InjectSequence(InputSequences.TypeText("Hello"));
app.InjectSequence(InputSequences.DoubleClick(new (5, 5)));
Troubleshooting
Events Not Firing
Symptom: Injected input doesn't trigger expected events
Checklist:
- Did you call
app.Init()before injection? - Is the view enabled and visible?
- Is
AutoProcessenabled (default: true)? - Are you using the correct coordinate system (viewport-relative)?
Debug:
// Enable trace logging to see pipeline flow
Logging.Trace($"Injecting: {key}");
app.Driver.KeyDown += (s, e) => Logging.Trace($"KeyDown: {e}");
Double-Clicks Not Detected
Symptom: Multi-click detection fails
Causes:
- Time not advanced between clicks
- Position changed between clicks
- Timing outside threshold (default 500ms)
Fix:
// Ensure timing is within threshold
app.InjectMouse(firstPress);
time.Advance(TimeSpan.FromMilliseconds(50));
app.InjectMouse(firstRelease);
time.Advance(TimeSpan.FromMilliseconds(300)); // < 500ms total
app.InjectMouse(secondPress); // SAME position!
time.Advance(TimeSpan.FromMilliseconds(50));
app.InjectMouse(secondRelease);
ANSI Encoding Fails
Symptom: Pipeline mode doesn't work correctly
Causes:
- Not using ANSI driver
- Parser not enabled in processor
- Invalid ANSI sequence encoding
Fix:
// Ensure ANSI driver and Pipeline mode
app.Init(DriverRegistry.Names.ANSI);
InputInjectionOptions options = new ()
{
Mode = InputInjectionMode.Pipeline
};
app.InjectKey(key, options);
Escape Key Behavior
Symptom: Escape key not processed correctly
Cause: ANSI parser holds Esc for 50ms to detect escape sequences
Solution: Virtual time automatically handles this via ProcessQueue():
// This automatically advances time past escape timeout
app.InjectKey(Key.Esc); // ProcessQueue handles timing internally
Performance Issues
Symptom: Tests run slowly
Causes:
- Using real time instead of virtual time
- Using Pipeline mode when Direct mode sufficient
- Too many small injections vs sequences
Fix:
// Use virtual time and Direct mode
VirtualTimeProvider time = new ();
app.InjectKey(key); // Fast Direct mode
// Use sequences for multiple inputs
app.InjectSequence(events); // Better than loop of InjectKey()