334e98f596
- 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
155 lines
5.0 KiB
C#
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);
|
|
}
|
|
}
|