Skip to content

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.0

The 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 ConsoleDriverFactory and any other changed Prompt settings in try/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 = true when testing Escape or a driver that invokes CancellationCallback; otherwise user cancellation exits the process. Token cancellation always throws OperationCanceledException.
  • The sample overrides ReadKey(CancellationToken) to avoid waiting and to fail on exhausted input. If relying on the interface's default implementation, KeyAvailable must 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.

Released under the MIT License.