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? 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 Inputs { get; } = []; public IReadOnlyList Render(int width) => ["submenu"]; public void HandleInput(TuiInputEvent input) => Inputs.Add(input); } }