using System.Collections.ObjectModel; using System.Reflection; using AtomUI.Controls.Data; using AtomUI.Controls.Utils; using Avalonia.Controls; using Shouldly; using Xunit; using AtomListView = AtomUI.Desktop.Controls.ListView; using AtomPagination = AtomUI.Desktop.Controls.Pagination; namespace AtomUI.Desktop.Controls.Tests.ListView; public class ListViewStateTests { static ListViewStateTests() { AvaloniaTestApp.EnsureInitialized(); } [Fact] public void ItemsSource_Wrap_Does_Not_Install_Default_Filter_Without_Active_Filtering() { var listView = new AtomListView { ItemsSource = CreateItems("alpha", "beta") }; var collectionView = listView.ItemsSource.ShouldBeAssignableTo(); collectionView.Filter.ShouldBeNull( "a ListView with no filter conditions should not force ListCollectionView into the local filtering path."); collectionView.FilterDescriptions.ShouldBeEmpty(); } [Fact] public void Default_Filter_Callback_Is_Attached_Only_While_Filter_Conditions_Are_Active() { var listView = new AtomListView { ItemsSource = CreateItems("alpha", "beta"), Filter = ValueFilterFactory.BuildFilter(ValueFilterMode.Contains) }; var collectionView = listView.ItemsSource.ShouldBeAssignableTo(); listView.FilterValue = "alpha"; collectionView.Filter.ShouldNotBeNull( "ListView needs its default callback only when FilterDescriptions should drive the collection view."); collectionView.FilterDescriptions.Count.ShouldBe(1); listView.FilterValue = null; collectionView.Filter.ShouldBeNull( "the default callback should be removed when ListView no longer owns active filter conditions."); collectionView.FilterDescriptions.ShouldBeEmpty(); } [Fact] public void Bottom_Pagination_State_Tracks_Collection_View_Changes() { var items = new ObservableCollection(CreateItems("alpha", "beta")); var pagination = new AtomPagination(); var listView = new AtomListView { PageSize = 10, ItemsSource = items, BottomPagination = pagination }; pagination.Total.ShouldBe(2); pagination.PageSize.ShouldBe(10); pagination.CurrentPage.ShouldBe(1); items.Add(new ListItemData { Content = "gamma" }); listView.TotalItemCount.ShouldBe(3); pagination.Total.ShouldBe(3); pagination.PageSize.ShouldBe(10); pagination.CurrentPage.ShouldBe(1); } [Fact] public void Pagination_Visibility_Does_Not_Override_Pagination_Motion_State() { var pagination = new AtomPagination(); var listView = new AtomListView { IsMotionEnabled = true, ItemsSource = CreateItems("alpha", "beta"), BottomPagination = pagination, PaginationVisibility = ListPaginationVisibility.None }; pagination.IsVisible.ShouldBeFalse(); pagination.IsMotionEnabled.ShouldBeTrue( "PaginationVisibility controls visibility only and must not be relayed into IsMotionEnabled."); listView.PaginationVisibility = ListPaginationVisibility.Bottom; pagination.IsVisible.ShouldBeTrue(); pagination.IsMotionEnabled.ShouldBeTrue(); } [Fact] public void ItemsSource_Replacement_Uses_The_New_Entry_Selection_Lifecycle() { var listView = new AtomListView { ItemsSource = CreateItems("alpha", "beta", "gamma"), SelectionMode = SelectionMode.AlwaysSelected }; listView.SelectedIndex = 1; listView.ItemsSource = CreateItems("delta", "epsilon", "zeta"); listView.SelectedIndex = 2; listView.SelectedIndex.ShouldBe(2, "the new source owns a fresh occurrence identity lifecycle."); } [Fact] public void Removing_Selected_Entry_Publishes_Deselection_With_Old_Item() { var first = new ListItemData { Content = "first" }; var second = new ListItemData { Content = "second" }; var source = new ObservableCollection { first, second }; var listView = new AtomListView { ItemsSource = source, SelectionMode = SelectionMode.Multiple }; ListViewSelectionChangedEventArgs? change = null; listView.SelectionChanged += (_, args) => { if (args.DeselectedItems.Count > 0) { change = args; } }; listView.Selection.Select(1); source.RemoveAt(1); change.ShouldNotBeNull(); change!.DeselectedItems.ShouldBe([second]); listView.SelectedItems.ShouldBeEmpty(); } [Fact] public void Virtualizing_Context_Tolerates_Duplicate_Recycled_Index() { var listView = new AtomListView(); var firstContainer = new AtomUI.Desktop.Controls.ListViewItem(); var secondContainer = new AtomUI.Desktop.Controls.ListViewItem(); var itemData = new ListItemData { Content = "item" }; PrepareContainerForItem(listView, firstContainer, itemData, 5); PrepareContainerForItem(listView, secondContainer, itemData, 5); ClearContainerForItem(listView, firstContainer); Should.NotThrow(() => ClearContainerForItem(listView, secondContainer), "virtualized collection mutations can recycle more than one stale container for the same index before one is restored."); } private static ListItemData[] CreateItems(params string[] values) { return values.Select(value => new ListItemData { Content = value }).ToArray(); } private static void PrepareContainerForItem( AtomListView listView, AtomUI.Desktop.Controls.ListViewItem container, object item, int index) { var method = typeof(AtomListView).GetMethod( "PrepareContainerForItemOverride", BindingFlags.Instance | BindingFlags.NonPublic); method.ShouldNotBeNull(); method.Invoke(listView, [container, item, index]); } private static void ClearContainerForItem( AtomListView listView, AtomUI.Desktop.Controls.ListViewItem container) { var method = typeof(AtomListView).GetMethod( "ClearContainerForItemOverride", BindingFlags.Instance | BindingFlags.NonPublic); method.ShouldNotBeNull(); method.Invoke(listView, [container]); } }