diff --git a/src/Sharprompt/Forms/MultiSelectForm.cs b/src/Sharprompt/Forms/MultiSelectForm.cs index ab6d816..e358ddb 100644 --- a/src/Sharprompt/Forms/MultiSelectForm.cs +++ b/src/Sharprompt/Forms/MultiSelectForm.cs @@ -124,32 +124,45 @@ private bool HandleSpacebar() private bool HandleCtrlA() { - if (_selectedItems.Count == Paginator.TotalCount) + // Bulk operations only apply to the currently filtered items, so selections + // hidden by the filter are preserved. + var selectedItems = new HashSet(_selectedItems); + + if (Paginator.All(_selectedItems.Contains)) { - _selectedItems.Clear(); + selectedItems.ExceptWith(Paginator); } else { - foreach (var item in Paginator) - { - _selectedItems.Add(item); - } + selectedItems.UnionWith(Paginator); } + UpdateSelectedItems(selectedItems); + return true; } private bool HandleCtrlI() { - var invertedItems = Paginator.Except(_selectedItems).ToArray(); + var selectedItems = new HashSet(_selectedItems); - _selectedItems.Clear(); + selectedItems.SymmetricExceptWith(Paginator); + + UpdateSelectedItems(selectedItems); - foreach (var item in invertedItems) + return true; + } + + private void UpdateSelectedItems(HashSet selectedItems) + { + if (selectedItems.Count > _options.Maximum) { - _selectedItems.Add(item); + SetError(string.Format(Resource.Validation_Maximum_SelectionRequired, _options.Maximum)); + + return; } - return true; + _selectedItems.Clear(); + _selectedItems.UnionWith(selectedItems); } } diff --git a/src/Sharprompt/Internal/Paginator.cs b/src/Sharprompt/Internal/Paginator.cs index b6e6add..753b478 100644 --- a/src/Sharprompt/Internal/Paginator.cs +++ b/src/Sharprompt/Internal/Paginator.cs @@ -93,7 +93,9 @@ public void PreviousItem() { if (_selectedIndex <= 0) { - if (!LoopingSelection) + // With nothing selected, select the last item on the current page + // instead of moving to the previous page, mirroring NextItem. + if (!LoopingSelection && _selectedIndex == 0) { PreviousPage(); } diff --git a/tests/Sharprompt.Tests/Forms/FormInteractionTests.cs b/tests/Sharprompt.Tests/Forms/FormInteractionTests.cs index 88a808a..9414999 100644 --- a/tests/Sharprompt.Tests/Forms/FormInteractionTests.cs +++ b/tests/Sharprompt.Tests/Forms/FormInteractionTests.cs @@ -123,6 +123,170 @@ public void MultiSelectForm_SpacebarToggle_ReturnsCheckedItems() Assert.Equal(new[] { "apple", "banana" }, form.Start()); } + [Fact] + public void MultiSelectForm_CtrlA_SelectsAllItems() + { + var (driver, configuration) = CreateTestContext(); + + driver.EnqueueKey(ConsoleKey.A, ConsoleModifiers.Control, '\x01'); + driver.EnqueueEnter(); + + var options = new MultiSelectOptions + { + Message = "message", + Items = ["apple", "banana", "cherry"] + }; + + using var form = new MultiSelectForm(options, configuration); + + Assert.Equal(new[] { "apple", "banana", "cherry" }, form.Start()); + } + + [Fact] + public void MultiSelectForm_CtrlA_AllSelected_DeselectsAllItems() + { + var (driver, configuration) = CreateTestContext(); + + driver.EnqueueKey(ConsoleKey.A, ConsoleModifiers.Control, '\x01'); + driver.EnqueueEnter(); + + var options = new MultiSelectOptions + { + Message = "message", + Items = ["apple", "banana", "cherry"], + DefaultValues = ["apple", "banana", "cherry"], + Minimum = 0 + }; + + using var form = new MultiSelectForm(options, configuration); + + Assert.Empty(form.Start()); + } + + [Fact] + public void MultiSelectForm_CtrlA_ExceedingMaximum_KeepsSelection() + { + var (driver, configuration) = CreateTestContext(); + + driver.EnqueueKey(ConsoleKey.A, ConsoleModifiers.Control, '\x01'); + driver.EnqueueEnter(); + + var options = new MultiSelectOptions + { + Message = "message", + Items = ["apple", "banana", "cherry"], + DefaultValues = ["apple"], + Maximum = 2 + }; + + using var form = new MultiSelectForm(options, configuration); + + Assert.Equal(new[] { "apple" }, form.Start()); + } + + [Fact] + public void MultiSelectForm_CtrlA_WithFilter_SelectsOnlyFilteredItems() + { + var (driver, configuration) = CreateTestContext(); + + driver.EnqueueText("b"); + driver.EnqueueKey(ConsoleKey.A, ConsoleModifiers.Control, '\x01'); + driver.EnqueueEnter(); + + var options = new MultiSelectOptions + { + Message = "message", + Items = ["apple", "banana", "cherry", "blueberry"] + }; + + using var form = new MultiSelectForm(options, configuration); + + Assert.Equal(new[] { "banana", "blueberry" }, form.Start()); + } + + [Fact] + public void MultiSelectForm_CtrlA_WithFilter_DeselectsOnlyFilteredItems() + { + var (driver, configuration) = CreateTestContext(); + + driver.EnqueueText("b"); + driver.EnqueueKey(ConsoleKey.A, ConsoleModifiers.Control, '\x01'); + driver.EnqueueEnter(); + + var options = new MultiSelectOptions + { + Message = "message", + Items = ["apple", "banana", "cherry", "blueberry"], + DefaultValues = ["apple", "banana", "blueberry"] + }; + + using var form = new MultiSelectForm(options, configuration); + + Assert.Equal(new[] { "apple" }, form.Start()); + } + + [Fact] + public void MultiSelectForm_CtrlI_InvertsSelection() + { + var (driver, configuration) = CreateTestContext(); + + driver.EnqueueKey(ConsoleKey.I, ConsoleModifiers.Control, '\t'); + driver.EnqueueEnter(); + + var options = new MultiSelectOptions + { + Message = "message", + Items = ["apple", "banana", "cherry"], + DefaultValues = ["apple"] + }; + + using var form = new MultiSelectForm(options, configuration); + + Assert.Equal(new[] { "banana", "cherry" }, form.Start()); + } + + [Fact] + public void MultiSelectForm_CtrlI_WithFilter_PreservesHiddenSelections() + { + var (driver, configuration) = CreateTestContext(); + + driver.EnqueueText("b"); + driver.EnqueueKey(ConsoleKey.I, ConsoleModifiers.Control, '\t'); + driver.EnqueueEnter(); + + var options = new MultiSelectOptions + { + Message = "message", + Items = ["apple", "banana", "cherry", "blueberry"], + DefaultValues = ["apple", "banana"] + }; + + using var form = new MultiSelectForm(options, configuration); + + Assert.Equal(new[] { "apple", "blueberry" }, form.Start()); + } + + [Fact] + public void MultiSelectForm_CtrlI_ExceedingMaximum_KeepsSelection() + { + var (driver, configuration) = CreateTestContext(); + + driver.EnqueueKey(ConsoleKey.I, ConsoleModifiers.Control, '\t'); + driver.EnqueueEnter(); + + var options = new MultiSelectOptions + { + Message = "message", + Items = ["apple", "banana", "cherry"], + DefaultValues = ["apple"], + Maximum = 1 + }; + + using var form = new MultiSelectForm(options, configuration); + + Assert.Equal(new[] { "apple" }, form.Start()); + } + [Fact] public void InputForm_TypedText_ReturnsConvertedValue() { diff --git a/tests/Sharprompt.Tests/Internal/PaginatorTests.cs b/tests/Sharprompt.Tests/Internal/PaginatorTests.cs index 35a3cd7..5163565 100644 --- a/tests/Sharprompt.Tests/Internal/PaginatorTests.cs +++ b/tests/Sharprompt.Tests/Internal/PaginatorTests.cs @@ -103,6 +103,39 @@ public void PreviousItem() Assert.Equal(0, selectedItem); } + [Fact] + public void PreviousItem_NotSelected_SelectsLastItemOnCurrentPage() + { + var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString()); + paginator.LoopingSelection = false; + + paginator.NextPage(); + paginator.PreviousItem(); + + var selected = paginator.TryGetSelectedItem(out var selectedItem); + + Assert.True(selected); + Assert.Equal(9, selectedItem); + Assert.Equal(1, paginator.CurrentPage); + } + + [Fact] + public void PreviousItem_FirstItem_MovesToPreviousPage() + { + var paginator = new Paginator(Enumerable.Range(0, 20), 5, Optional.Empty, x => x.ToString()); + paginator.LoopingSelection = false; + + paginator.NextPage(); + paginator.NextItem(); + paginator.PreviousItem(); + + var selected = paginator.TryGetSelectedItem(out var selectedItem); + + Assert.True(selected); + Assert.Equal(4, selectedItem); + Assert.Equal(0, paginator.CurrentPage); + } + [Fact] public void PreviousPage() {