diff --git a/docs/input/cli/unit-testing.md b/docs/input/cli/unit-testing.md index 05c940e..bef67f3 100644 --- a/docs/input/cli/unit-testing.md +++ b/docs/input/cli/unit-testing.md @@ -63,6 +63,90 @@ The following example validates the exit code and terminal output of a `Spectre. } ``` +The following example demonstrates how to mock user inputs for an interactive command. +This test (InteractiveCommand_WithMockedUserInputs_ProducesExpectedOutput) simulates user interactions by pushing predefined inputs to the console, then verifies that the resulting output is as expected. + +```csharp +public sealed class InteractiveCommandTests +{ + private sealed class InteractiveCommand : Command + { + private readonly IAnsiConsole _console; + + public InteractiveCommand(IAnsiConsole console) + { + _console = console; + } + + public override int Execute(CommandContext context) + { + var fruits = _console.Prompt( + new MultiSelectionPrompt() + .Title("What are your [green]favorite fruits[/]?") + .NotRequired() // Not required to have a favorite fruit + .PageSize(10) + .MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]") + .InstructionsText( + "[grey](Press [blue][/] to toggle a fruit, " + + "[green][/] to accept)[/]") + .AddChoices(new[] { + "Apple", "Apricot", "Avocado", + "Banana", "Blackcurrant", "Blueberry", + "Cherry", "Cloudberry", "Coconut", + })); + + var fruit = _console.Prompt( + new SelectionPrompt() + .Title("What's your [green]favorite fruit[/]?") + .PageSize(10) + .MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]") + .AddChoices(new[] { + "Apple", "Apricot", "Avocado", + "Banana", "Blackcurrant", "Blueberry", + "Cherry", "Cloudberry", "Cocunut", + })); + + var name = _console.Ask("What's your name?"); + + _console.WriteLine($"[{string.Join(',', fruits)};{fruit};{name}]"); + + return 0; + } + } + + [Fact] + public void InteractiveCommand_WithMockedUserInputs_ProducesExpectedOutput() + { + // Given + TestConsole console = new(); + console.Interactive(); + + // Your mocked inputs must always end with "Enter" for each prompt! + + // Multi selection prompt: Choose first option + console.Input.PushKey(ConsoleKey.Spacebar); + console.Input.PushKey(ConsoleKey.Enter); + + // Selection prompt: Choose second option + console.Input.PushKey(ConsoleKey.DownArrow); + console.Input.PushKey(ConsoleKey.Enter); + + // Ask text prompt: Enter name + console.Input.PushTextWithEnter("Spectre Console"); + + var app = new CommandAppTester(null, new CommandAppTesterSettings(), console); + app.SetDefaultCommand(); + + // When + var result = app.Run(); + + // Then + result.ExitCode.ShouldBe(0); + result.Output.EndsWith("[Apple;Apricot;Spectre Console]"); + } +} +``` + ## Testing console behaviour `TestConsole` and `TestConsoleInput` are testable implementations of `IAnsiConsole` and `IAnsiConsoleInput`, allowing you fine-grain control over testing console output and interactivity. diff --git a/src/Spectre.Console.Testing/Cli/CommandAppTester.cs b/src/Spectre.Console.Testing/Cli/CommandAppTester.cs index 597dba1..3817903 100644 --- a/src/Spectre.Console.Testing/Cli/CommandAppTester.cs +++ b/src/Spectre.Console.Testing/Cli/CommandAppTester.cs @@ -8,6 +8,11 @@ public sealed class CommandAppTester private Action? _appConfiguration; private Action? _configuration; + /// + /// Gets the test console used by both the CommandAppTester and CommandApp. + /// + public TestConsole Console { get; } + /// /// Gets or sets the Registrar to use in the CommandApp. /// @@ -23,10 +28,15 @@ public sealed class CommandAppTester /// /// The registrar. /// The settings. - public CommandAppTester(ITypeRegistrar? registrar = null, CommandAppTesterSettings? settings = null) + /// The test console that overrides the default one. + public CommandAppTester( + ITypeRegistrar? registrar = null, + CommandAppTesterSettings? settings = null, + TestConsole? console = null) { Registrar = registrar; TestSettings = settings ?? new CommandAppTesterSettings(); + Console = console ?? new TestConsole().Width(int.MaxValue); } /// @@ -36,6 +46,7 @@ public sealed class CommandAppTester public CommandAppTester(CommandAppTesterSettings settings) { TestSettings = settings; + Console = new TestConsole().Width(int.MaxValue); } /// @@ -85,25 +96,23 @@ public sealed class CommandAppTester public CommandAppFailure RunAndCatch(params string[] args) where T : Exception { - var console = new TestConsole().Width(int.MaxValue); - try { - Run(args, console, c => c.PropagateExceptions()); + Run(args, Console, c => c.PropagateExceptions()); throw new InvalidOperationException("Expected an exception to be thrown, but there was none."); } catch (T ex) { if (ex is CommandAppException commandAppException && commandAppException.Pretty != null) { - console.Write(commandAppException.Pretty); + Console.Write(commandAppException.Pretty); } else { - console.WriteLine(ex.Message); + Console.WriteLine(ex.Message); } - return new CommandAppFailure(ex, console.Output); + return new CommandAppFailure(ex, Console.Output); } catch (Exception ex) { @@ -120,8 +129,7 @@ public sealed class CommandAppTester /// The result. public CommandAppResult Run(params string[] args) { - var console = new TestConsole().Width(int.MaxValue); - return Run(args, console); + return Run(args, Console); } private CommandAppResult Run(string[] args, TestConsole console, Action? config = null) @@ -164,8 +172,7 @@ public sealed class CommandAppTester /// The result. public async Task RunAsync(params string[] args) { - var console = new TestConsole().Width(int.MaxValue); - return await RunAsync(args, console); + return await RunAsync(args, Console); } private async Task RunAsync(string[] args, TestConsole console, Action? config = null) diff --git a/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/CommandAppTesterTests.cs b/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/CommandAppTesterTests.cs index 6772862..7af0d4c 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/CommandAppTesterTests.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/CommandAppTesterTests.cs @@ -44,4 +44,40 @@ public sealed class CommandAppTesterTests // Then result.Output.ShouldBe(expected); } + + [Fact] + public void DefaultCtor_WithoutParameters_CreatesDefaultConsole() + { + // Given, When + CommandAppTester app = new(); + + // Then + app.Console.ShouldNotBeNull(); + app.Console.Profile.Width.ShouldBe(int.MaxValue); + } + + [Fact] + public void DefaultCtor_WithCustomConsole_UsesProvidedInstance() + { + // Given + TestConsole console = new(); + + // When + CommandAppTester app = new(null, new CommandAppTesterSettings(), console); + + // Then + app.Console.ShouldNotBeNull(); + app.Console.ShouldBeSameAs(console); + } + + [Fact] + public void Ctor_WithSettings_CreatesDefaultConsole() + { + // Given, When + CommandAppTester app = new(new CommandAppTesterSettings()); + + // Then + app.Console.ShouldNotBeNull(); + app.Console.Profile.Width.ShouldBe(int.MaxValue); + } } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/InteractiveCommandTests.cs b/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/InteractiveCommandTests.cs new file mode 100644 index 0000000..0feb7e2 --- /dev/null +++ b/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/InteractiveCommandTests.cs @@ -0,0 +1,80 @@ +namespace Spectre.Console.Cli.Tests.Unit.Testing; + +public sealed class InteractiveCommandTests +{ + private sealed class InteractiveCommand : Command + { + private readonly IAnsiConsole _console; + + public InteractiveCommand(IAnsiConsole console) + { + _console = console; + } + + public override int Execute(CommandContext context) + { + var fruits = _console.Prompt( + new MultiSelectionPrompt() + .Title("What are your [green]favorite fruits[/]?") + .NotRequired() // Not required to have a favorite fruit + .PageSize(10) + .MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]") + .InstructionsText( + "[grey](Press [blue][/] to toggle a fruit, " + + "[green][/] to accept)[/]") + .AddChoices(new[] { + "Apple", "Apricot", "Avocado", + "Banana", "Blackcurrant", "Blueberry", + "Cherry", "Cloudberry", "Coconut", + })); + + var fruit = _console.Prompt( + new SelectionPrompt() + .Title("What's your [green]favorite fruit[/]?") + .PageSize(10) + .MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]") + .AddChoices(new[] { + "Apple", "Apricot", "Avocado", + "Banana", "Blackcurrant", "Blueberry", + "Cherry", "Cloudberry", "Cocunut", + })); + + var name = _console.Ask("What's your name?"); + + _console.WriteLine($"[{string.Join(',', fruits)};{fruit};{name}]"); + + return 0; + } + } + + [Fact] + public void InteractiveCommand_WithMockedUserInputs_ProducesExpectedOutput() + { + // Given + TestConsole console = new(); + console.Interactive(); + + // Your mocked inputs must always end with "Enter" for each prompt! + + // Multi selection prompt: Choose first option + console.Input.PushKey(ConsoleKey.Spacebar); + console.Input.PushKey(ConsoleKey.Enter); + + // Selection prompt: Choose second option + console.Input.PushKey(ConsoleKey.DownArrow); + console.Input.PushKey(ConsoleKey.Enter); + + // Ask text prompt: Enter name + console.Input.PushTextWithEnter("Spectre Console"); + + var app = new CommandAppTester(null, new CommandAppTesterSettings(), console); + app.SetDefaultCommand(); + + // When + var result = app.Run(); + + // Then + result.ExitCode.ShouldBe(0); + result.Output.EndsWith("[Apple;Apricot;Spectre Console]"); + } +}