|
8 | 8 | using Aspire.Cli.Utils; |
9 | 9 | using Microsoft.Extensions.Logging.Abstractions; |
10 | 10 | using Spectre.Console; |
| 11 | +using Spectre.Console.Rendering; |
11 | 12 |
|
12 | 13 | using System.Text; |
13 | 14 |
|
@@ -1038,4 +1039,112 @@ public void MakeSafeFormatter_WithEscapedMarkupAndBracketsInData_HandlesCorrectl |
1038 | 1039 | // Assert - markup is stripped, brackets in data are replaced |
1039 | 1040 | Assert.Equal("Service (Prod)", result); |
1040 | 1041 | } |
| 1042 | + |
| 1043 | + [Theory] |
| 1044 | + [InlineData(true, "[Y/n]")] |
| 1045 | + [InlineData(false, "[y/N]")] |
| 1046 | + public async Task ConfirmAsync_DisplaysCapitalizedDefaultChoice(bool defaultValue, string expectedChoiceSuffix) |
| 1047 | + { |
| 1048 | + // Arrange - simulate pressing Enter (accepts default) |
| 1049 | + var output = new StringBuilder(); |
| 1050 | + var console = CreateInteractiveConsoleWithInput(output, "\n"); |
| 1051 | + var executionContext = new CliExecutionContext(new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); |
| 1052 | + var interactionService = CreateInteractionService(console, executionContext); |
| 1053 | + |
| 1054 | + // Act |
| 1055 | + await interactionService.ConfirmAsync("Proceed?", defaultValue, CancellationToken.None); |
| 1056 | + |
| 1057 | + // Assert - the output should contain the [Y/n] or [y/N] suffix |
| 1058 | + var outputString = output.ToString(); |
| 1059 | + Assert.Contains(expectedChoiceSuffix, outputString); |
| 1060 | + } |
| 1061 | + |
| 1062 | + [Theory] |
| 1063 | + [InlineData(true)] |
| 1064 | + [InlineData(false)] |
| 1065 | + public async Task ConfirmAsync_WhenUserPressesEnter_ReturnsDefaultValue(bool defaultValue) |
| 1066 | + { |
| 1067 | + // Arrange - simulate pressing Enter (empty input selects default) |
| 1068 | + var output = new StringBuilder(); |
| 1069 | + var console = CreateInteractiveConsoleWithInput(output, "\n"); |
| 1070 | + var executionContext = new CliExecutionContext(new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); |
| 1071 | + var interactionService = CreateInteractionService(console, executionContext); |
| 1072 | + |
| 1073 | + // Act |
| 1074 | + var result = await interactionService.ConfirmAsync("Proceed?", defaultValue, CancellationToken.None); |
| 1075 | + |
| 1076 | + // Assert - pressing Enter should accept the default value |
| 1077 | + Assert.Equal(defaultValue, result); |
| 1078 | + } |
| 1079 | + |
| 1080 | + private static IAnsiConsole CreateInteractiveConsoleWithInput(StringBuilder output, string input) |
| 1081 | + { |
| 1082 | + var settings = new AnsiConsoleSettings |
| 1083 | + { |
| 1084 | + Ansi = AnsiSupport.No, |
| 1085 | + ColorSystem = ColorSystemSupport.NoColors, |
| 1086 | + Interactive = InteractionSupport.Yes, |
| 1087 | + Out = new AnsiConsoleOutput(new StringWriter(output)), |
| 1088 | + }; |
| 1089 | + var console = AnsiConsole.Create(settings); |
| 1090 | + console.Profile.Width = int.MaxValue; |
| 1091 | + return new TestAnsiConsoleWithInput(console, new StringReader(input)); |
| 1092 | + } |
| 1093 | +} |
| 1094 | + |
| 1095 | +/// <summary> |
| 1096 | +/// A test <see cref="IAnsiConsole"/> wrapper that redirects input reads to a <see cref="TextReader"/>, |
| 1097 | +/// allowing prompts to be answered in unit tests without blocking on real console input. |
| 1098 | +/// </summary> |
| 1099 | +file sealed class TestAnsiConsoleWithInput : IAnsiConsole |
| 1100 | +{ |
| 1101 | + private readonly IAnsiConsole _inner; |
| 1102 | + private readonly IAnsiConsoleInput _testInput; |
| 1103 | + |
| 1104 | + public TestAnsiConsoleWithInput(IAnsiConsole inner, TextReader inputReader) |
| 1105 | + { |
| 1106 | + _inner = inner; |
| 1107 | + _testInput = new TextReaderInput(inputReader); |
| 1108 | + } |
| 1109 | + |
| 1110 | + public Profile Profile => _inner.Profile; |
| 1111 | + public IAnsiConsoleCursor Cursor => _inner.Cursor; |
| 1112 | + public IAnsiConsoleInput Input => _testInput; |
| 1113 | + public IExclusivityMode ExclusivityMode => _inner.ExclusivityMode; |
| 1114 | + public RenderPipeline Pipeline => _inner.Pipeline; |
| 1115 | + |
| 1116 | + public void Clear(bool home) => _inner.Clear(home); |
| 1117 | + public void Write(IRenderable renderable) => _inner.Write(renderable); |
| 1118 | + |
| 1119 | + private sealed class TextReaderInput : IAnsiConsoleInput |
| 1120 | + { |
| 1121 | + private readonly TextReader _reader; |
| 1122 | + |
| 1123 | + public TextReaderInput(TextReader reader) => _reader = reader; |
| 1124 | + |
| 1125 | + public bool IsKeyAvailable() => true; |
| 1126 | + |
| 1127 | + public ConsoleKeyInfo? ReadKey(bool intercept) |
| 1128 | + { |
| 1129 | + var read = _reader.Read(); |
| 1130 | + if (read == -1) |
| 1131 | + { |
| 1132 | + // End of stream - return Enter as a safe fallback |
| 1133 | + return new ConsoleKeyInfo('\r', ConsoleKey.Enter, shift: false, alt: false, control: false); |
| 1134 | + } |
| 1135 | + |
| 1136 | + var ch = (char)read; |
| 1137 | + var key = ch switch |
| 1138 | + { |
| 1139 | + '\n' or '\r' => ConsoleKey.Enter, |
| 1140 | + 'y' or 'Y' => ConsoleKey.Y, |
| 1141 | + 'n' or 'N' => ConsoleKey.N, |
| 1142 | + _ => ConsoleKey.Enter, |
| 1143 | + }; |
| 1144 | + return new ConsoleKeyInfo(ch, key, shift: char.IsUpper(ch), alt: false, control: false); |
| 1145 | + } |
| 1146 | + |
| 1147 | + public Task<ConsoleKeyInfo?> ReadKeyAsync(bool intercept, CancellationToken cancellationToken) |
| 1148 | + => Task.FromResult(ReadKey(intercept)); |
| 1149 | + } |
1041 | 1150 | } |
0 commit comments