# 自動テスト

既定のコンソールドライバーは標準入出力のリダイレクトを拒否します。CI や AI コーディング支援による検証には、`Prompt.ConsoleDriverFactory` で `IConsoleDriver` を差し替えます。対話端末なしで公開 API とソース生成したバインディングを検証できます。

## 完全なサンプルを実行する

リポジトリのルートで実行します。

```sh
dotnet run --project samples/Sharprompt.Automation -f net8.0
dotnet run --project samples/Sharprompt.Automation -f net10.0
```

Input、Select、MultiSelect、Confirm、Password、List、バリデーション後の再入力、モデルバインディング、開始後のキャンセルを検証します。結果が異なる場合やキー入力が不足した場合は直ちに失敗します。CI でも両フレームワークを実行します。以下のコードはコンパイル対象のサンプルから直接取り込んでいます。

## Program.cs

```csharp
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

```csharp
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; } = () => { };
}
```

## 自分のテストへの組み込み

- `ConsoleDriverFactory` などの `Prompt` 設定はプロセス全体で共有されます。変更前の値を保存し、`try` / `finally` で復元します。同じ設定を使うテストは並列実行を無効にしてください。
- 各プロンプトはドライバーを所有し、終了時に破棄します。`Bind` の各プロパティも含め、毎回新しいドライバーを返します。プロパティごとに入力が異なる場合は、ドライバーのスクリプトをキューで管理します。
- Escape や `CancellationCallback` を使うドライバーのテストでは `ThrowExceptionOnCancel = true` にします。既定ではユーザーのキャンセル操作がプロセスを終了します。トークンによるキャンセルは常に `OperationCanceledException` です。
- サンプルは `ReadKey(CancellationToken)` を実装して待機を避け、入力不足を例外にします。インターフェイスの既定実装を使う場合、`KeyAvailable` を正確に実装しないと、キャンセル可能なプロンプトが待機し続けることがあります。
- この小さなドライバーは短い ASCII 入力向けです。最終画面ではなく書き込みを記録し、ANSI スタイル、Unicode の表示幅、折り返し、スクロールは再現しません。描画結果の検証には、より完全な端末エミュレーターを使用してください。

## README の例の検証

`dotnet test -c Release` は README のすべての C# ブロックも、標準的なコンソールプロジェクトの暗黙の using を持つ独立プログラムとしてコンパイルします。両ソースジェネレーターも実行します。対話プログラム自体は起動しません。API の変更や、トップレベルステートメントを型宣言の後に置く誤りを検出できます。
