@@ -2,7 +2,7 @@ namespace TimeWarp.Terminal;
22
33/// <summary>
44/// Provides an ambient context for <see cref="TestTerminal"/> that enables zero-configuration testing
5- /// of CLI applications.
5+ /// of CLI applications with automatic <see cref="TimeWarp.Terminal.Terminal.Instance"/> synchronization .
66/// </summary>
77/// <remarks>
88/// <para>
@@ -11,38 +11,58 @@ namespace TimeWarp.Terminal;
1111/// running tests in parallel.
1212/// </para>
1313/// <para>
14+ /// When <see cref="Current"/> is set, it automatically updates <see cref="TimeWarp.Terminal.Terminal.Instance"/>
15+ /// and restores the previous instance when cleared.
16+ /// </para>
17+ /// <para>
1418/// Resolution order when determining which terminal to use:
1519/// <list type="number">
1620/// <item><description><see cref="Current"/> (if set)</description></item>
1721/// <item><description><c>ITerminal</c> from DI (if registered)</description></item>
1822/// <item><description><see cref="TimeWarpTerminal.Default"/> (fallback)</description></item>
1923/// </list>
2024/// </para>
25+ /// </remarks>
2126/// <example>
27+ /// Simple test pattern with automatic TimeWarp.Terminal.Terminal.Instance synchronization:
2228/// <code>
2329/// public static async Task Should_display_greeting()
2430/// {
2531/// using TestTerminal terminal = new();
2632/// TestTerminalContext.Current = terminal;
2733///
34+ /// // TimeWarp.Terminal.Terminal.Instance is now set to terminal
35+ /// Terminal.WriteLine("Hello"); // Routes to test terminal
36+ ///
2837/// await Program.Main(["greet", "World"]);
2938///
3039/// terminal.OutputContains("Hello, World!").ShouldBeTrue();
40+ ///
41+ /// // On dispose, TestTerminal clears context and restores previous TimeWarp.Terminal.Terminal.Instance
3142/// }
3243/// </code>
3344/// </example>
34- /// </remarks>
3545public static class TestTerminalContext
3646{
3747 private static readonly AsyncLocal < TestTerminal ? > Context = new ( ) ;
48+ private static readonly AsyncLocal < ITerminal ? > PreviousInstance = new ( ) ;
3849
3950 /// <summary>
4051 /// Gets or sets the current <see cref="TestTerminal"/> for the async execution context.
4152 /// </summary>
4253 /// <remarks>
4354 /// <para>
44- /// Setting this property to a non-null value causes all terminal resolution
45- /// to use the provided <see cref="TestTerminal"/> instead of the real terminal.
55+ /// Setting this property to a non-null value causes:
56+ /// <list type="bullet">
57+ /// <item><description>The current <see cref="TimeWarp.Terminal.Terminal.Instance"/> to be saved</description></item>
58+ /// <item><description><see cref="TimeWarp.Terminal.Terminal.Instance"/> to be set to the provided terminal</description></item>
59+ /// </list>
60+ /// </para>
61+ /// <para>
62+ /// Setting this property to <c>null</c> causes:
63+ /// <list type="bullet">
64+ /// <item><description><see cref="TimeWarp.Terminal.Terminal.Instance"/> to be restored to its previous value</description></item>
65+ /// </list>
4666 /// </para>
4767 /// <para>
4868 /// The value is scoped to the current async execution context, so parallel tests
@@ -55,14 +75,41 @@ public static class TestTerminalContext
5575 public static TestTerminal ? Current
5676 {
5777 get => Context . Value ;
58- set => Context . Value = value ;
78+ set
79+ {
80+ if ( value is not null )
81+ {
82+ // Save current TimeWarp.Terminal.Terminal.Instance before replacing
83+ PreviousInstance . Value = TimeWarp . Terminal . Terminal . Instance ;
84+ Context . Value = value ;
85+ TimeWarp . Terminal . Terminal . Instance = value ;
86+ }
87+ else if ( Context . Value is not null )
88+ {
89+ // Restore previous TimeWarp.Terminal.Terminal.Instance
90+ Context . Value = null ;
91+ if ( PreviousInstance . Value is not null )
92+ {
93+ TimeWarp . Terminal . Terminal . Instance = PreviousInstance . Value ;
94+ PreviousInstance . Value = null ;
95+ }
96+ }
97+ }
5998 }
6099
61100 /// <summary>
62101 /// Gets a value indicating whether a <see cref="TestTerminal"/> is set for the current context.
63102 /// </summary>
64103 public static bool HasValue => Context . Value is not null ;
65104
105+ /// <summary>
106+ /// Gets the <see cref="TestTerminal"/> for the current context, or throws if not set.
107+ /// </summary>
108+ /// <returns>The current <see cref="TestTerminal"/>.</returns>
109+ /// <exception cref="InvalidOperationException">Thrown when no test terminal is set.</exception>
110+ public static TestTerminal Terminal
111+ => Context . Value ?? throw new InvalidOperationException ( "No TestTerminal set in current context. Set TestTerminalContext.Current first." ) ;
112+
66113 /// <summary>
67114 /// Resolves a terminal using the standard resolution order:
68115 /// TestTerminalContext.Current → provided terminal → fallback.
0 commit comments