Skip to content
Prev Previous commit
Next Next commit
Fix help TUI: set Markdown.Text in Initialized handler, add integrati…
…on tests

The help command's TUI viewer rendered empty content because
Markdown.Text was set before the view was initialized/laid out.
Following the pattern from gui-cs/clet's MarkdownClet, set the text
inside the Initialized event handler on the containing Runnable.

Also switches from RunnableWrapper to a plain Runnable with an embedded
Markdown view — the help viewer is read-only and needs no result
extraction.

Adds integration tests using Application.Create()/Init(ansi) with
StopAfterFirstIteration to verify:
- The TUI renders without hanging
- Driver contents contain expected command text
- Subcommand help renders correctly
- RenderCatAsync produces ANSI output

Co-authored-by: Copilot <[email protected]>
  • Loading branch information
tig and Copilot committed May 24, 2026
commit 98efef3d145b7031e97d5e2ef73aeeb655cf7e30
25 changes: 21 additions & 4 deletions src/Terminal.Gui.Cli/HelpCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Terminal.Gui.App;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;

namespace Terminal.Gui.Cli;
Expand Down Expand Up @@ -43,11 +44,27 @@ public async Task<CommandResult> RunAsync (IApplication app, string? initial, Co
{
var markdown = ResolveHelp (options);

RunnableWrapper<Markdown, object?> wrapper = new ();
wrapper.GetWrappedView ().Text = markdown;
wrapper.Title = options.Title ?? "Help";
Runnable window = new ()
{
Title = options.Title ?? "Help",
Width = Dim.Fill (),
Height = Dim.Fill ()
};

Markdown markdownView = new ()
{
Width = Dim.Fill (),
Height = Dim.Fill ()
};

window.Add (markdownView);

window.Initialized += (_, _) =>
{
markdownView.Text = markdown;
};

await app.RunAsync (wrapper, cancellationToken);
await app.RunAsync (window, cancellationToken);

return new CommandResult (CommandStatus.Ok, null, null, null);
}
Expand Down
152 changes: 152 additions & 0 deletions tests/Terminal.Gui.Cli.IntegrationTests/HelpCommandIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using Terminal.Gui.App;
using Terminal.Gui.Drivers;
using Xunit;

namespace Terminal.Gui.Cli.IntegrationTests;

public sealed class HelpCommandIntegrationTests
{
[Fact]
public async Task RunAsync_WithStopAfterFirstIteration_RendersMarkdownInViewer ()
{
using IApplication app = Application.Create ();
app.Init (DriverRegistry.Names.ANSI);
app.StopAfterFirstIteration = true;

CommandRegistry registry = new ();
MetadataHelpProvider helpProvider = new ();
HelpCommand helpCommand = new (registry, helpProvider);
registry.Register (helpCommand);

CommandRunOptions options = new ();

CommandResult result = await helpCommand.RunAsync (app, null, options, CancellationToken.None);

Assert.Equal (CommandStatus.Ok, result.Status);
}

[Fact]
public async Task RunAsync_CancellationToken_AlreadyCancelled_DoesNotHang ()
{
using IApplication app = Application.Create ();
app.Init (DriverRegistry.Names.ANSI);

CommandRegistry registry = new ();
MetadataHelpProvider helpProvider = new ();
HelpCommand helpCommand = new (registry, helpProvider);
registry.Register (helpCommand);

CommandRunOptions options = new ();

using CancellationTokenSource cts = new ();
await cts.CancelAsync ();

// Should either throw OperationCanceledException or return quickly
try
{
await helpCommand.RunAsync (app, null, options, cts.Token);
}
catch (OperationCanceledException)
{
// Expected
}
}

[Fact]
public async Task RunAsync_RendersHelpText_ContainingCommandName ()
{
using IApplication app = Application.Create ();
app.Init (DriverRegistry.Names.ANSI);
app.StopAfterFirstIteration = true;

// Set screen size for deterministic rendering
app.Driver!.SetScreenSize (80, 24);

CommandRegistry registry = new ();
MetadataHelpProvider helpProvider = new ();
HelpCommand helpCommand = new (registry, helpProvider);
registry.Register (helpCommand);

CommandRunOptions options = new ();

CommandResult result = await helpCommand.RunAsync (app, null, options, CancellationToken.None);

Assert.Equal (CommandStatus.Ok, result.Status);

// Verify the driver rendered content containing the "help" command
var driverContents = app.Driver.ToString ();
Assert.Contains ("help", driverContents);
}

[Fact]
public async Task RunAsync_WithSubcommandArgument_RendersCommandHelp ()
{
using IApplication app = Application.Create ();
app.Init (DriverRegistry.Names.ANSI);
app.StopAfterFirstIteration = true;

app.Driver!.SetScreenSize (80, 24);

CommandRegistry registry = new ();
MetadataHelpProvider helpProvider = new ();
HelpCommand helpCommand = new (registry, helpProvider);
registry.Register (helpCommand);
registry.Register (new StubCommand ("greet", "Say hello."));

CommandRunOptions options = new ()
{
Arguments = ["greet"]
};

CommandResult result = await helpCommand.RunAsync (app, null, options, CancellationToken.None);

Assert.Equal (CommandStatus.Ok, result.Status);

var driverContents = app.Driver.ToString ();
Assert.Contains ("greet", driverContents);
}

[Fact]
public async Task RenderCatAsync_ProducesAnsiOutput ()
{
CommandRegistry registry = new ();
MetadataHelpProvider helpProvider = new ();
HelpCommand helpCommand = new (registry, helpProvider);
registry.Register (helpCommand);

CommandRunOptions options = new ();
using StringWriter stdout = new ();

CommandResult? result = await helpCommand.RenderCatAsync (options, stdout, CancellationToken.None);

Assert.NotNull (result);
Assert.Equal (CommandStatus.Ok, result.Value.Status);

var output = stdout.ToString ();

// ANSI escape sequences present
Assert.Contains ("\x1b[", output);
Assert.Contains ("help", output);
}

private sealed class StubCommand (string alias, string description) : ICliCommand
{
public string PrimaryAlias { get; } = alias;

public IReadOnlyList<string> Aliases => [PrimaryAlias];

public string Description => description;

public CommandKind Kind => CommandKind.Input;

public Type ResultType => typeof (string);

public IReadOnlyList<CommandOptionDescriptor> Options { get; } = [];

public Task<CommandResult> RunAsync (IApplication app, string? initial, CommandRunOptions options,
CancellationToken cancellationToken)
{
return Task.FromResult (new CommandResult (CommandStatus.Ok, "ok", null, null));
}
}
}
16 changes: 16 additions & 0 deletions tests/Terminal.Gui.Cli.IntegrationTests/TestSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Runtime.CompilerServices;

namespace Terminal.Gui.Cli.IntegrationTests;

/// <summary>
/// Disables real driver I/O so tests never interact with the terminal or launch processes.
/// </summary>
internal static class TestSetup
{
[ModuleInitializer]
internal static void Init ()
{
Environment.SetEnvironmentVariable ("DisableRealDriverIO", "1");
Console.SetIn (TextReader.Null);
}
}
Loading