Files
ttui/test/TinyTUI.Tests/Components/SettingsListTests.cs
T
chuan 334e98f596 feat: add settings list component
- add SettingsList with search, value cycling and submenu delegation

- add fuzzy filtering helper for settings search

- cover settings list rendering and input behavior with regression tests
2026-06-04 09:39:40 +08:00

155 lines
5.0 KiB
C#

using TinyTUI.Components;
using TinyTUI.Input;
using TinyTUI.Tests.TestInfrastructure;
using TinyTUI.Text;
namespace TinyTUI.Tests.Components;
public sealed class SettingsListTests
{
private readonly TerminalTextMeasurer _measurer = new();
[Fact]
public void RenderShowsLabelsValuesSearchAndWrappedSelectedDescription()
{
var list = new SettingsList(
[
new SettingItem("theme", "Theme", "Light", "Controls the terminal palette and contrast for every settings row", ["Light", "Dark"]),
new SettingItem("mode", "Mode", "Auto"),
],
_measurer)
{
SearchEnabled = true,
Focused = true,
};
var lines = list.Render(34);
AnsiAssert.ContainsVisible("Search:", lines[0]);
Assert.Contains(TinyTUI.Rendering.CursorMarker.Marker, lines[0], StringComparison.Ordinal);
AnsiAssert.ContainsVisible("> Theme Light", lines[2]);
Assert.Contains(lines, line => StripAnsiContains(line, "Controls the terminal"));
Assert.Contains(lines, line => StripAnsiContains(line, "Type to search"));
}
[Fact]
public void InputMovesSelectionCancelsAndCyclesValuesWithEnterOrSpace()
{
var changes = new List<(string Id, string Value)>();
var canceled = false;
var item = new SettingItem("theme", "Theme", "Light", values: ["Light", "Dark"]);
var list = new SettingsList([item, new SettingItem("mode", "Mode", "Auto")], _measurer)
{
OnChanged = (id, value) => changes.Add((id, value)),
OnCanceled = () => canceled = true,
};
list.HandleInput(Key(KeyNames.Down));
Assert.Equal(1, list.SelectedIndex);
list.HandleInput(Key(KeyNames.Up));
list.HandleInput(Key(KeyNames.Enter));
Assert.Equal("Dark", item.CurrentValue);
list.HandleInput(Key(" "));
Assert.Equal("Light", item.CurrentValue);
list.HandleInput(Key(KeyNames.Escape));
Assert.True(canceled);
Assert.Equal([("theme", "Dark"), ("theme", "Light")], changes);
}
[Fact]
public void SearchFiltersWithFuzzyMatcherAndBackspaceRestoresResults()
{
var list = new SettingsList(
[
new SettingItem("theme", "Theme Color", "Light"),
new SettingItem("telemetry", "Telemetry", "Off"),
new SettingItem("mode", "Mode", "Auto"),
],
_measurer)
{
SearchEnabled = true,
};
list.HandleInput(Text("tc"));
Assert.Equal("tc", list.SearchQuery);
Assert.Equal("Theme Color", list.SelectedItem?.Label);
Assert.DoesNotContain(list.Render(60), line => StripAnsiContains(line, "Telemetry"));
list.HandleInput(Key(KeyNames.Backspace));
Assert.Equal("t", list.SearchQuery);
Assert.Contains(list.Render(60), line => StripAnsiContains(line, "Telemetry"));
}
[Fact]
public void SubmenuDelegatesInputAndDoneClosesWithSelectionRestored()
{
Action<string?>? done = null;
var child = new RecordingSubmenu();
var item = new SettingItem(
"profile",
"Profile",
"Default",
submenu: (currentValue, callback) =>
{
child.SeenValue = currentValue;
done = callback;
return child;
});
var list = new SettingsList([new SettingItem("theme", "Theme", "Light"), item], _measurer);
var changes = new List<(string Id, string Value)>();
list.OnChanged = (id, value) => changes.Add((id, value));
list.HandleInput(Key(KeyNames.Down));
list.HandleInput(Key(KeyNames.Enter));
Assert.Same(child, list.ActiveSubmenu);
Assert.Equal("Default", child.SeenValue);
Assert.Single(list.GetChildren());
list.HandleInput(Key(KeyNames.Down));
Assert.Equal(KeyNames.Down, child.Inputs.Single().Value);
done?.Invoke("Custom");
Assert.Null(list.ActiveSubmenu);
Assert.Empty(list.GetChildren());
Assert.Equal(1, list.SelectedIndex);
Assert.Equal("Custom", item.CurrentValue);
Assert.Equal([("profile", "Custom")], changes);
}
private static TuiInputEvent Key(string value) => new(TuiInputEventKind.Key, value);
private static TuiInputEvent Text(string value) => new(TuiInputEventKind.Text, value);
private static bool StripAnsiContains(string line, string expected)
{
try
{
AnsiAssert.ContainsVisible(expected, line);
return true;
}
catch
{
return false;
}
}
private sealed class RecordingSubmenu : IInputComponent
{
public string? SeenValue { get; set; }
public List<TuiInputEvent> Inputs { get; } = [];
public IReadOnlyList<string> Render(int width) => ["submenu"];
public void HandleInput(TuiInputEvent input) => Inputs.Add(input);
}
}