English
Automated Testing
The default console driver rejects redirected standard input or output. For CI or coding-agent verification, inject an IConsoleDriver using Prompt.ConsoleDriverFactory. This exercises the public prompts and source-generated binding without a terminal.
Run the Complete Example
From the repository root:
sh
dotnet run --project samples/Sharprompt.Automation -f net8.0
dotnet run --project samples/Sharprompt.Automation -f net10.0The example checks Input, Select, MultiSelect, Confirm, Password, List, validation retries, model binding, and cancellation after a prompt starts. An unexpected answer or exhausted input fails immediately. CI runs both frameworks. The following source is imported from that compiled sample.
Program.cs
cs
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using Sharprompt;
using Sharprompt.Automation;
// This program runs without a terminal. Unexpected answers or exhausted input fail the process.
var previousFactory = Prompt.ConsoleDriverFactory;
var previousCancel = Prompt.ThrowExceptionOnCancel;
try
{
Prompt.ThrowExceptionOnCancel = true;
// A prompt owns and disposes its driver. Queue a fresh one for each prompt.
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Type("Ada").Enter();
var name = Prompt.Input<string>("Name", validators: [Validators.Required()]);
if (name != "Ada")
{
throw new InvalidOperationException("Unexpected input result.");
}
// No item is highlighted initially: the first Down selects Seattle, the second Tokyo.
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Key(ConsoleKey.DownArrow).Key(ConsoleKey.DownArrow).Enter();
var city = Prompt.Select("City", new[] { "Seattle", "Tokyo" });
if (city != "Tokyo")
{
throw new InvalidOperationException("Unexpected selection.");
}
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Key(ConsoleKey.DownArrow).Key(ConsoleKey.Spacebar, ' ').Enter();
if (!Prompt.MultiSelect("Cities", new[] { "Tokyo", "London" }).SequenceEqual(new[] { "Tokyo" }))
{
throw new InvalidOperationException("Unexpected multiple selection.");
}
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Enter();
if (!Prompt.Confirm("Continue?", defaultValue: true))
{
throw new InvalidOperationException("Unexpected confirmation.");
}
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Type("example-password").Enter();
if (Prompt.Password("Password", validators: [Validators.MinLength(8)]) != "example-password")
{
throw new InvalidOperationException("Unexpected password.");
}
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Type("42").Enter().Enter();
if (!Prompt.List<int>("Numbers").SequenceEqual(new[] { 42 }))
{
throw new InvalidOperationException("Unexpected list.");
}
// The first Enter fails validation; the same prompt then accepts the scripted text.
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Enter().Type("Ada").Enter();
if (Prompt.Input<string>("Required name", validators: [Validators.Required()]) != "Ada")
{
throw new InvalidOperationException("Validation retry failed.");
}
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Type("Ada").Enter();
if (Prompt.Bind<Profile>().Name != "Ada")
{
throw new InvalidOperationException("Source-generated binding failed.");
}
// Exercise cancellation after the prompt has started, with no timer or sleep.
using var cancellation = new CancellationTokenSource();
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver { OnRead = cancellation.Cancel };
try
{
Prompt.Input<string>(options => options.Message = "Canceled input", cancellation.Token);
throw new InvalidOperationException("Expected token cancellation.");
}
catch (OperationCanceledException ex) when (ex.CancellationToken == cancellation.Token)
{
}
}
finally
{
Prompt.ConsoleDriverFactory = previousFactory;
Prompt.ThrowExceptionOnCancel = previousCancel;
}
Console.WriteLine("All automation examples passed.");
[PromptBindable]
public class Profile
{
[Required]
public string Name { get; set; } = "";
}ScriptedConsoleDriver.cs
cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Sharprompt.Drivers;
namespace Sharprompt.Automation;
// A small test driver for the short, ASCII input in this sample. It records rendered text,
// rather than emulating a terminal screen, ANSI styles, Unicode widths, or scrolling.
internal sealed class ScriptedConsoleDriver : IConsoleDriver
{
private readonly Queue<ConsoleKeyInfo> _keys = new();
private readonly StringBuilder _output = new();
public string Output => _output.ToString();
public Action? OnRead { get; init; }
public ScriptedConsoleDriver Key(ConsoleKey key, char keyChar = '\0')
{
_keys.Enqueue(new ConsoleKeyInfo(keyChar, key, false, false, false));
return this;
}
public ScriptedConsoleDriver Type(string text)
{
foreach (var character in text)
{
Key(default, character);
}
return this;
}
public ScriptedConsoleDriver Enter() => Key(ConsoleKey.Enter, '\r');
public ConsoleKeyInfo ReadKey() => ReadKey(CancellationToken.None);
public ConsoleKeyInfo ReadKey(CancellationToken cancellationToken)
{
OnRead?.Invoke();
cancellationToken.ThrowIfCancellationRequested();
// Fail promptly when a prompt unexpectedly needs more input, instead of hanging CI.
return _keys.Count > 0 ? _keys.Dequeue() : throw new InvalidOperationException("Scripted input exhausted.");
}
public void Write(string value, ConsoleColor color)
{
_output.Append(value);
CursorLeft += value.Length;
}
public void WriteLine()
{
_output.Append('\n');
CursorLeft = 0;
CursorTop++;
}
public void SetCursorPosition(int left, int top)
{
CursorLeft = left;
CursorTop = top;
}
public void ClearLine(int top) => SetCursorPosition(0, top);
public void Beep() { }
public void Reset() { }
public void Dispose() { }
// Render after each key. ReadKey(token) is overridden so cancellation never polls here.
public bool KeyAvailable => false;
public bool CursorVisible { set { } }
public int CursorLeft { get; private set; }
public int CursorTop { get; private set; }
public int BufferWidth => 500;
public int BufferHeight => 1000;
public int WindowWidth => 500;
public int WindowHeight => 1000;
public Action CancellationCallback { get; set; } = () => { };
}Adapting It to Your Tests
- Save and restore
ConsoleDriverFactoryand any other changedPromptsettings intry/finally. They are process-wide. Disable parallel execution for tests sharing these settings. - Each prompt owns and disposes its driver. Return a fresh driver for each prompt, including each property in
Bind. Use a queue of driver scripts for models with different property inputs. - Set
ThrowExceptionOnCancel = truewhen testing Escape or a driver that invokesCancellationCallback; otherwise user cancellation exits the process. Token cancellation always throwsOperationCanceledException. - The sample overrides
ReadKey(CancellationToken)to avoid waiting and to fail on exhausted input. If relying on the interface's default implementation,KeyAvailablemust accurately report queued keys or a cancelable prompt can wait forever. - This small driver supports short ASCII scenarios and records writes rather than a final terminal screen. It does not emulate ANSI styles, Unicode column widths, wrapping, or scrolling. Use a fuller terminal emulator for rendering assertions.
README Examples
dotnet test -c Release also compiles every C# block in the README as a separate console program with the standard console-project implicit usings. Both source generators run during this check. The tests do not launch those interactive programs. This catches API drift and invalid examples, including top-level statements placed after type declarations.