diff --git a/CliFx.Tests/ConsoleSpecs.cs b/CliFx.Tests/ConsoleSpecs.cs index 6341dab..c319bad 100644 --- a/CliFx.Tests/ConsoleSpecs.cs +++ b/CliFx.Tests/ConsoleSpecs.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using CliFx.Infrastructure; using CliFx.Tests.Utils; +using CliFx.Tests.Utils.Extensions; using CliWrap; using CliWrap.Buffered; using FluentAssertions; @@ -139,6 +140,53 @@ public class Command : ICommand stdErr.Trim().Should().Be("Hello world"); } + [Fact] + public async Task Fake_console_can_read_key_presses() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" +[Command] +public class Command : ICommand +{ + public ValueTask ExecuteAsync(IConsole console) + { + console.Output.WriteLine(console.ReadKey().Key); + console.Output.WriteLine(console.ReadKey().Key); + console.Output.WriteLine(console.ReadKey().Key); + + return default; + } +} +"); + + var application = new CliApplicationBuilder() + .AddCommand(commandType) + .UseConsole(FakeConsole) + .Build(); + + // Act + FakeConsole.EnqueueKey(new ConsoleKeyInfo('0', ConsoleKey.D0, false, false, false)); + FakeConsole.EnqueueKey(new ConsoleKeyInfo('a', ConsoleKey.A, false, false, false)); + FakeConsole.EnqueueKey(new ConsoleKeyInfo('\0', ConsoleKey.Backspace, false, false, false)); + + var exitCode = await application.RunAsync( + Array.Empty(), + new Dictionary() + ); + + var stdOut = FakeConsole.ReadOutputString(); + + // Assert + exitCode.Should().Be(0); + stdOut.Trim().Should().ConsistOfLines( + "D0", + "A", + "Backspace" + ); + } + [Fact] public void Console_does_not_emit_preamble_when_used_with_encoding_that_has_it() { diff --git a/CliFx/Infrastructure/FakeConsole.cs b/CliFx/Infrastructure/FakeConsole.cs index 9ba8289..7e9a6aa 100644 --- a/CliFx/Infrastructure/FakeConsole.cs +++ b/CliFx/Infrastructure/FakeConsole.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.IO; using System.Threading; @@ -14,6 +15,7 @@ namespace CliFx.Infrastructure; public class FakeConsole : IConsole, IDisposable { private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly ConcurrentQueue _keys = new(); /// public ConsoleReader Input { get; } @@ -91,9 +93,18 @@ public class FakeConsole : IConsole, IDisposable } /// - public void ReadKey(bool intercept = false) - { - } + public ConsoleKeyInfo ReadKey(bool intercept = false) => + _keys.TryDequeue(out var key) + ? key + : throw new InvalidOperationException( + "Cannot read key because there are no key presses enqueued. " + + $"Use the `{nameof(EnqueueKey)}(...)` method to simulate a key press." + ); + + /// + /// Enqueues a simulated key press, which can then be read by calling . + /// + public void EnqueueKey(ConsoleKeyInfo key) => _keys.Enqueue(key); /// public virtual void Dispose() => _cancellationTokenSource.Dispose(); diff --git a/CliFx/Infrastructure/IConsole.cs b/CliFx/Infrastructure/IConsole.cs index 2d79e79..694af6e 100644 --- a/CliFx/Infrastructure/IConsole.cs +++ b/CliFx/Infrastructure/IConsole.cs @@ -91,7 +91,7 @@ public interface IConsole /// /// Obtains the next character or function key pressed by the user. /// - void ReadKey(bool intercept = false); + ConsoleKeyInfo ReadKey(bool intercept = false); } /// diff --git a/CliFx/Infrastructure/SystemConsole.cs b/CliFx/Infrastructure/SystemConsole.cs index a1d430b..385d20a 100644 --- a/CliFx/Infrastructure/SystemConsole.cs +++ b/CliFx/Infrastructure/SystemConsole.cs @@ -94,7 +94,7 @@ public class SystemConsole : IConsole, IDisposable public void Clear() => Console.Clear(); /// - public void ReadKey(bool intercept = false) => Console.ReadKey(intercept); + public ConsoleKeyInfo ReadKey(bool intercept = false) => Console.ReadKey(intercept); /// public void Dispose()