From 961af98e5ce9a21b78ed55bae18b32b825e0c089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Sun, 8 Sep 2024 19:51:07 +0200 Subject: [PATCH] Obsolete the async prompt APIs The current prompt APIs suffer from several problems. First, the prompt APIs are fundamentally synchronous APIs. If we get down to the underlying implementation of all the prompt APIs, i.e. the `DefaultInput` class we can see some issues. Here's are the problematic implementation (before this commit fixes it): ```csharp public ConsoleKeyInfo? ReadKey(bool intercept) { return System.Console.ReadKey(intercept); } public async Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) { while (true) { if (cancellationToken.IsCancellationRequested) { return null; } if (System.Console.KeyAvailable) { break; } await Task.Delay(5, cancellationToken).ConfigureAwait(false); } return ReadKey(intercept); } ``` * The syncrhonous `ReadKey` method returns a nullable `ConsoleKeyInfo` struct but `System.Console.ReadKey` can never return a nullable `ConsoleKeyInfo`. * The asyncrhonous `ReadKeyAsync` method can return `null` only if cancellation has been requested. But this can never actually happen since [Fix deadlock when cancelling prompts (#1439)](https://github.com/spectreconsole/spectre.console/pull/1439) was merged. * The asyncrhonous `ReadKeyAsync` method is not actually an synchronous method, it's waiting for a key to be pressed in a loop, waiting 5 milliseconds before checking again if it can break out of the loop. The proposed fix obsoletes the `ReadKeyAsync` method and add cancellation support to the synchronous `ReadKey` method through an optional `CancellationToken`. It also returns a non-nullable `ConsoleKeyInfo` making it clear that the only way to get out of this method is through cancellation. Then this change bubbles up to all the prompt APIs, also obsoleting the `IPrompt.ShowAsync` methods. This is a better alternative to [Async overloads for AnsiConsole Prompt/Ask/Confirm (#1194)](https://github.com/spectreconsole/spectre.console/pull/1194) where the actual need is having a `CancellationToken` to perform some cleanup and not having async prompt APIs. Note that this is a breaking change since it modifies the signatures of the public `IAnsiConsoleInput` interface but that should not be an issue since it's impossible to use another implentation than `DefaultInput` when used through `AnsiConsole.Create(AnsiConsoleSettings settings)`. I have also searched for [implementers of IAnsiConsoleInput](https://grep.app/search?q=IAnsiConsoleInput) and I think this change won't break anything since nobody actually implemented `IAnsiConsoleInput`. Only exising implementations which have been udated are being used (at least across a half million public git repos). The addition of the `CancellationToken` to `IPrompt.Show(IAnsiConsole console, CancellationToken cancellationToken = default)` is also a breaking change but it should be mitigated since it has bee introduced with a default value. --- docs/input/prompts/multiselection.md | 3 + docs/input/prompts/selection.md | 2 +- .../TestConsoleInput.cs | 7 ++- src/Spectre.Console/AnsiConsole.Prompt.cs | 20 ++++--- .../Extensions/AnsiConsoleExtensions.Input.cs | 11 +--- .../AnsiConsoleExtensions.Prompt.cs | 22 ++++--- src/Spectre.Console/IAnsiConsoleInput.cs | 16 +++-- src/Spectre.Console/Internal/DefaultInput.cs | 34 +++-------- .../Prompts/ConfirmationPrompt.cs | 17 +++--- src/Spectre.Console/Prompts/IPrompt.cs | 4 +- .../Prompts/List/ListPrompt.cs | 11 +--- .../Prompts/MultiSelectionPrompt.cs | 17 +++--- .../Prompts/SelectionPrompt.cs | 17 +++--- src/Spectre.Console/Prompts/TextPrompt.cs | 22 +++---- .../Unit/Prompts/CancellationTests.cs | 60 +++++++++++++++++++ 15 files changed, 161 insertions(+), 102 deletions(-) create mode 100644 src/Tests/Spectre.Console.Tests/Unit/Prompts/CancellationTests.cs diff --git a/docs/input/prompts/multiselection.md b/docs/input/prompts/multiselection.md index 4e11b2613..90551c075 100644 --- a/docs/input/prompts/multiselection.md +++ b/docs/input/prompts/multiselection.md @@ -5,6 +5,9 @@ Highlights: - Display multiple items for a user to scroll and choose from. - Custom page sizes. - Provide groups of selectable items. +Reference: + - T:Spectre.Console.MultiSelectionPrompt`1 + - M:Spectre.Console.AnsiConsole.Prompt``1(Spectre.Console.IPrompt{``0},System.Threading.CancellationToken) --- The `MultiSelectionPrompt` can be used when you want the user to select diff --git a/docs/input/prompts/selection.md b/docs/input/prompts/selection.md index 150b32806..83fac7177 100644 --- a/docs/input/prompts/selection.md +++ b/docs/input/prompts/selection.md @@ -3,7 +3,7 @@ Order: 1 Description: "The **SelectionPrompt** can be used when you want the user to select a single item from a provided list." Reference: - T:Spectre.Console.SelectionPrompt`1 - - M:Spectre.Console.AnsiConsole.Prompt``1(Spectre.Console.IPrompt{``0}) + - M:Spectre.Console.AnsiConsole.Prompt``1(Spectre.Console.IPrompt{``0},System.Threading.CancellationToken) --- The `SelectionPrompt` can be used when you want the user to select diff --git a/src/Spectre.Console.Testing/TestConsoleInput.cs b/src/Spectre.Console.Testing/TestConsoleInput.cs index e471d30ed..8c8a0f364 100644 --- a/src/Spectre.Console.Testing/TestConsoleInput.cs +++ b/src/Spectre.Console.Testing/TestConsoleInput.cs @@ -77,18 +77,21 @@ public bool IsKeyAvailable() } /// - public ConsoleKeyInfo? ReadKey(bool intercept) + public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken = default) { if (_input.Count == 0) { throw new InvalidOperationException("No input available."); } + cancellationToken.ThrowIfCancellationRequested(); + return _input.Dequeue(); } /// - public Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) + [Obsolete("This method will be removed in a future release. Use the synchronous ReadKey() method instead.", error: false)] + public Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) { return Task.FromResult(ReadKey(intercept)); } diff --git a/src/Spectre.Console/AnsiConsole.Prompt.cs b/src/Spectre.Console/AnsiConsole.Prompt.cs index ef892ae9e..266b079f4 100644 --- a/src/Spectre.Console/AnsiConsole.Prompt.cs +++ b/src/Spectre.Console/AnsiConsole.Prompt.cs @@ -10,15 +10,16 @@ public static partial class AnsiConsole /// /// The prompt result type. /// The prompt to display. + /// The token to monitor for cancellation requests. /// The prompt input result. - public static T Prompt(IPrompt prompt) + public static T Prompt(IPrompt prompt, CancellationToken cancellationToken = default) { if (prompt is null) { throw new ArgumentNullException(nameof(prompt)); } - return prompt.Show(Console); + return prompt.Show(Console, cancellationToken); } /// @@ -26,10 +27,11 @@ public static T Prompt(IPrompt prompt) /// /// The prompt result type. /// The prompt markup text. + /// The token to monitor for cancellation requests. /// The prompt input result. - public static T Ask(string prompt) + public static T Ask(string prompt, CancellationToken cancellationToken = default) { - return new TextPrompt(prompt).Show(Console); + return new TextPrompt(prompt).Show(Console, cancellationToken); } /// @@ -38,12 +40,13 @@ public static T Ask(string prompt) /// The prompt result type. /// The prompt markup text. /// The default value. + /// The token to monitor for cancellation requests. /// The prompt input result. - public static T Ask(string prompt, T defaultValue) + public static T Ask(string prompt, T defaultValue, CancellationToken cancellationToken = default) { return new TextPrompt(prompt) .DefaultValue(defaultValue) - .Show(Console); + .Show(Console, cancellationToken); } /// @@ -51,13 +54,14 @@ public static T Ask(string prompt, T defaultValue) /// /// The prompt markup text. /// Specifies the default answer. + /// The token to monitor for cancellation requests. /// true if the user selected "yes", otherwise false. - public static bool Confirm(string prompt, bool defaultValue = true) + public static bool Confirm(string prompt, bool defaultValue = true, CancellationToken cancellationToken = default) { return new ConfirmationPrompt(prompt) { DefaultValue = defaultValue, } - .Show(Console); + .Show(Console, cancellationToken); } } \ No newline at end of file diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs index 2d6a4cb0b..cc3f1e847 100644 --- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs +++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs @@ -5,7 +5,7 @@ namespace Spectre.Console; /// public static partial class AnsiConsoleExtensions { - internal static async Task ReadLine(this IAnsiConsole console, Style? style, bool secret, char? mask, IEnumerable? items = null, CancellationToken cancellationToken = default) + internal static string ReadLine(this IAnsiConsole console, Style? style, bool secret, char? mask, IEnumerable? items = null, CancellationToken cancellationToken = default) { if (console is null) { @@ -19,14 +19,7 @@ internal static async Task ReadLine(this IAnsiConsole console, Style? st while (true) { - cancellationToken.ThrowIfCancellationRequested(); - var rawKey = await console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false); - if (rawKey == null) - { - continue; - } - - var key = rawKey.Value; + var key = console.Input.ReadKey(true, cancellationToken); if (key.Key == ConsoleKey.Enter) { return text; diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Prompt.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Prompt.cs index 382503f8b..002704d1f 100644 --- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Prompt.cs +++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Prompt.cs @@ -11,15 +11,16 @@ public static partial class AnsiConsoleExtensions /// The prompt result type. /// The console. /// The prompt to display. + /// The token to monitor for cancellation requests. /// The prompt input result. - public static T Prompt(this IAnsiConsole console, IPrompt prompt) - { + public static T Prompt(this IAnsiConsole console, IPrompt prompt, CancellationToken cancellationToken = default) + { if (prompt is null) { throw new ArgumentNullException(nameof(prompt)); } - return prompt.Show(console); + return prompt.Show(console, cancellationToken); } /// @@ -28,10 +29,11 @@ public static T Prompt(this IAnsiConsole console, IPrompt prompt) /// The prompt result type. /// The console. /// The prompt markup text. + /// The token to monitor for cancellation requests. /// The prompt input result. - public static T Ask(this IAnsiConsole console, string prompt) + public static T Ask(this IAnsiConsole console, string prompt, CancellationToken cancellationToken = default) { - return new TextPrompt(prompt).Show(console); + return new TextPrompt(prompt).Show(console, cancellationToken); } /// @@ -41,12 +43,13 @@ public static T Ask(this IAnsiConsole console, string prompt) /// The console. /// The prompt markup text. /// Specific CultureInfo to use when converting input. + /// The token to monitor for cancellation requests. /// The prompt input result. - public static T Ask(this IAnsiConsole console, string prompt, CultureInfo? culture) + public static T Ask(this IAnsiConsole console, string prompt, CultureInfo? culture, CancellationToken cancellationToken = default) { var textPrompt = new TextPrompt(prompt); textPrompt.Culture = culture; - return textPrompt.Show(console); + return textPrompt.Show(console, cancellationToken); } /// @@ -55,13 +58,14 @@ public static T Ask(this IAnsiConsole console, string prompt, CultureInfo? cu /// The console. /// The prompt markup text. /// Specifies the default answer. + /// The token to monitor for cancellation requests. /// true if the user selected "yes", otherwise false. - public static bool Confirm(this IAnsiConsole console, string prompt, bool defaultValue = true) + public static bool Confirm(this IAnsiConsole console, string prompt, bool defaultValue = true, CancellationToken cancellationToken = default) { return new ConfirmationPrompt(prompt) { DefaultValue = defaultValue, } - .Show(console); + .Show(console, cancellationToken); } } \ No newline at end of file diff --git a/src/Spectre.Console/IAnsiConsoleInput.cs b/src/Spectre.Console/IAnsiConsoleInput.cs index 9bc5a12cb..7e55b394e 100644 --- a/src/Spectre.Console/IAnsiConsoleInput.cs +++ b/src/Spectre.Console/IAnsiConsoleInput.cs @@ -15,15 +15,23 @@ public interface IAnsiConsoleInput /// /// Reads a key from the console. /// - /// Whether or not to intercept the key. + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// + /// The token to monitor for cancellation requests. /// The key that was read. - ConsoleKeyInfo? ReadKey(bool intercept); + ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken = default); /// /// Reads a key from the console. /// - /// Whether or not to intercept the key. + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// /// The token to monitor for cancellation requests. /// The key that was read. - Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken); + [Obsolete("This method will be removed in a future release. Use the synchronous ReadKey() method instead.", error: false)] + Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Spectre.Console/Internal/DefaultInput.cs b/src/Spectre.Console/Internal/DefaultInput.cs index 423285c80..fb3af265e 100644 --- a/src/Spectre.Console/Internal/DefaultInput.cs +++ b/src/Spectre.Console/Internal/DefaultInput.cs @@ -19,38 +19,22 @@ public bool IsKeyAvailable() return System.Console.KeyAvailable; } - public ConsoleKeyInfo? ReadKey(bool intercept) + public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) { - if (!_profile.Capabilities.Interactive) + cancellationToken.ThrowIfCancellationRequested(); + + while (!IsKeyAvailable()) { - throw new InvalidOperationException("Failed to read input in non-interactive mode."); + cancellationToken.ThrowIfCancellationRequested(); + Thread.Sleep(5); } return System.Console.ReadKey(intercept); } - public async Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) + [Obsolete("This method will be removed in a future release. Use the synchronous ReadKey() method instead.", error: true)] + public Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) { - if (!_profile.Capabilities.Interactive) - { - throw new InvalidOperationException("Failed to read input in non-interactive mode."); - } - - while (true) - { - if (cancellationToken.IsCancellationRequested) - { - return null; - } - - if (System.Console.KeyAvailable) - { - break; - } - - await Task.Delay(5, cancellationToken).ConfigureAwait(false); - } - - return ReadKey(intercept); + return Task.FromResult(ReadKey(intercept, cancellationToken)); } } \ No newline at end of file diff --git a/src/Spectre.Console/Prompts/ConfirmationPrompt.cs b/src/Spectre.Console/Prompts/ConfirmationPrompt.cs index 5257d5233..b108f6c84 100644 --- a/src/Spectre.Console/Prompts/ConfirmationPrompt.cs +++ b/src/Spectre.Console/Prompts/ConfirmationPrompt.cs @@ -68,13 +68,7 @@ public ConfirmationPrompt(string prompt) } /// - public bool Show(IAnsiConsole console) - { - return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult(); - } - - /// - public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + public bool Show(IAnsiConsole console, CancellationToken cancellationToken = default) { var comparer = Comparer ?? StringComparer.CurrentCultureIgnoreCase; @@ -89,8 +83,15 @@ public async Task ShowAsync(IAnsiConsole console, CancellationToken cancel .AddChoice(Yes) .AddChoice(No); - var result = await prompt.ShowAsync(console, cancellationToken).ConfigureAwait(false); + var result = prompt.Show(console, cancellationToken); return comparer.Compare(Yes.ToString(), result.ToString()) == 0; } + + /// + [Obsolete("This method will be removed in a future release. Use the synchronous Show() method instead.", error: false)] + public Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + { + return Task.FromResult(Show(console, cancellationToken)); + } } \ No newline at end of file diff --git a/src/Spectre.Console/Prompts/IPrompt.cs b/src/Spectre.Console/Prompts/IPrompt.cs index 36ed44c46..9e0602a9f 100644 --- a/src/Spectre.Console/Prompts/IPrompt.cs +++ b/src/Spectre.Console/Prompts/IPrompt.cs @@ -10,8 +10,9 @@ public interface IPrompt /// Shows the prompt. /// /// The console. + /// The token to monitor for cancellation requests. /// The prompt input result. - T Show(IAnsiConsole console); + T Show(IAnsiConsole console, CancellationToken cancellationToken = default); /// /// Shows the prompt asynchronously. @@ -19,5 +20,6 @@ public interface IPrompt /// The console. /// The token to monitor for cancellation requests. /// The prompt input result. + [Obsolete("This method will be removed in a future release. Use the synchronous Show() method instead.", error: false)] Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Spectre.Console/Prompts/List/ListPrompt.cs b/src/Spectre.Console/Prompts/List/ListPrompt.cs index c66850256..5be878cc5 100644 --- a/src/Spectre.Console/Prompts/List/ListPrompt.cs +++ b/src/Spectre.Console/Prompts/List/ListPrompt.cs @@ -12,7 +12,7 @@ public ListPrompt(IAnsiConsole console, IListPromptStrategy strategy) _strategy = strategy ?? throw new ArgumentNullException(nameof(strategy)); } - public async Task> Show( + public ListPromptState Show( ListPromptTree tree, Func converter, SelectionMode selectionMode, @@ -52,14 +52,7 @@ public async Task> Show( while (true) { - cancellationToken.ThrowIfCancellationRequested(); - var rawKey = await _console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false); - if (rawKey == null) - { - continue; - } - - var key = rawKey.Value; + var key = _console.Input.ReadKey(true, cancellationToken); var result = _strategy.HandleInput(key, state); if (result == ListPromptInputResult.Submit) { diff --git a/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs b/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs index ffc3f4e58..32c8f3a9c 100644 --- a/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs +++ b/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs @@ -84,18 +84,12 @@ public IMultiSelectionItem AddChoice(T item) } /// - public List Show(IAnsiConsole console) - { - return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult(); - } - - /// - public async Task> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + public List Show(IAnsiConsole console, CancellationToken cancellationToken = default) { // Create the list prompt var prompt = new ListPrompt(console, this); var converter = Converter ?? TypeConverterHelper.ConvertToString; - var result = await prompt.Show(Tree, converter, Mode, false, false, PageSize, WrapAround, cancellationToken).ConfigureAwait(false); + var result = prompt.Show(Tree, converter, Mode, false, false, PageSize, WrapAround, cancellationToken); if (Mode == SelectionMode.Leaf) { @@ -111,6 +105,13 @@ public async Task> ShowAsync(IAnsiConsole console, CancellationToken can .ToList(); } + /// + [Obsolete("This method will be removed in a future release. Use the synchronous Show() method instead.", error: false)] + public Task> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + { + return Task.FromResult(Show(console, cancellationToken)); + } + /// /// Returns all parent items of the given . /// diff --git a/src/Spectre.Console/Prompts/SelectionPrompt.cs b/src/Spectre.Console/Prompts/SelectionPrompt.cs index c9f55690e..3818a69df 100644 --- a/src/Spectre.Console/Prompts/SelectionPrompt.cs +++ b/src/Spectre.Console/Prompts/SelectionPrompt.cs @@ -89,23 +89,24 @@ public ISelectionItem AddChoice(T item) } /// - public T Show(IAnsiConsole console) - { - return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult(); - } - - /// - public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + public T Show(IAnsiConsole console, CancellationToken cancellationToken = default) { // Create the list prompt var prompt = new ListPrompt(console, this); var converter = Converter ?? TypeConverterHelper.ConvertToString; - var result = await prompt.Show(_tree, converter, Mode, true, SearchEnabled, PageSize, WrapAround, cancellationToken).ConfigureAwait(false); + var result = prompt.Show(_tree, converter, Mode, true, SearchEnabled, PageSize, WrapAround, cancellationToken); // Return the selected item return result.Items[result.Index].Data; } + /// + [Obsolete("This method will be removed in a future release. Use the synchronous Show() method instead.", error: false)] + public Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + { + return Task.FromResult(Show(console, cancellationToken)); + } + /// ListPromptInputResult IListPromptStrategy.HandleInput(ConsoleKeyInfo key, ListPromptState state) { diff --git a/src/Spectre.Console/Prompts/TextPrompt.cs b/src/Spectre.Console/Prompts/TextPrompt.cs index 9574df175..745edf2c2 100644 --- a/src/Spectre.Console/Prompts/TextPrompt.cs +++ b/src/Spectre.Console/Prompts/TextPrompt.cs @@ -104,22 +104,17 @@ public TextPrompt(string prompt, StringComparer? comparer = null) /// Shows the prompt and requests input from the user. /// /// The console to show the prompt in. + /// The token to monitor for cancellation requests. /// The user input converted to the expected type. /// - public T Show(IAnsiConsole console) - { - return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult(); - } - - /// - public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + public T Show(IAnsiConsole console, CancellationToken cancellationToken = default) { if (console is null) { throw new ArgumentNullException(nameof(console)); } - return await console.RunExclusive(async () => + return console.RunExclusive(() => { var promptStyle = PromptStyle ?? Style.Plain; var converter = Converter ?? TypeConverterHelper.ConvertToString; @@ -130,7 +125,7 @@ public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellat while (true) { - var input = await console.ReadLine(promptStyle, IsSecret, Mask, choices, cancellationToken).ConfigureAwait(false); + var input = console.ReadLine(promptStyle, IsSecret, Mask, choices, cancellationToken); // Nothing entered? if (string.IsNullOrWhiteSpace(input)) @@ -182,7 +177,14 @@ public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellat return result; } - }).ConfigureAwait(false); + }); + } + + /// + [Obsolete("This method will be removed in a future release. Use the synchronous Show() method instead.", error: false)] + public Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + { + return Task.FromResult(Show(console, cancellationToken)); } /// diff --git a/src/Tests/Spectre.Console.Tests/Unit/Prompts/CancellationTests.cs b/src/Tests/Spectre.Console.Tests/Unit/Prompts/CancellationTests.cs new file mode 100644 index 000000000..6b206e424 --- /dev/null +++ b/src/Tests/Spectre.Console.Tests/Unit/Prompts/CancellationTests.cs @@ -0,0 +1,60 @@ +namespace Spectre.Console.Tests.Unit; + +public class CancellationTests +{ + private readonly IAnsiConsole _console = AnsiConsole.Create(new AnsiConsoleSettings { Interactive = InteractionSupport.Yes, Ansi = AnsiSupport.Yes }); + + [Fact] + public void ConfirmationPrompt_Should_Support_Cancellation() + { + // Given + var prompt = new ConfirmationPrompt(""); + + // When + Action action = () => prompt.Show(_console, new CancellationToken(canceled: true)); + + // Then + action.ShouldThrow(); + } + + [Fact] + public void TextPrompt_Should_Support_Cancellation() + { + // Given + var prompt = new TextPrompt(""); + + // When + Action action = () => prompt.Show(_console, new CancellationToken(canceled: true)); + + // Then + action.ShouldThrow(); + } + + [Fact] + public void MultiSelectionPrompt_Should_Support_Cancellation() + { + // Given + var prompt = new MultiSelectionPrompt(); + prompt.AddChoice(""); + + // When + Action action = () => prompt.Show(_console, new CancellationToken(canceled: true)); + + // Then + action.ShouldThrow(); + } + + [Fact] + public void SelectionPrompt_Should_Support_Cancellation() + { + // Given + var prompt = new SelectionPrompt(); + prompt.AddChoice(""); + + // When + Action action = () => prompt.Show(_console, new CancellationToken(canceled: true)); + + // Then + action.ShouldThrow(); + } +} \ No newline at end of file