using System.Collections.ObjectModel; using AtomUI.Controls; using AtomUI.Controls.Data; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Threading; using Shouldly; using Xunit; using AvaloniaWindow = Avalonia.Controls.Window; namespace AtomUI.Desktop.Controls.Tests.Transfer; public class TransferBehaviorTests { static TransferBehaviorTests() { AvaloniaTestApp.EnsureInitialized(); } [Fact] public void Key_Value_Properties_Are_TwoWay() { var selectedKeysMetadata = AbstractTransfer.SelectedKeysProperty.GetMetadata(typeof(AbstractTransfer)); var targetKeysMetadata = AbstractTransfer.TargetKeysProperty.GetMetadata(typeof(AbstractTransfer)); selectedKeysMetadata.DefaultBindingMode.ShouldBe(BindingMode.TwoWay); targetKeysMetadata.DefaultBindingMode.ShouldBe(BindingMode.TwoWay); } [Fact] public void TargetKeys_ObservableCollection_Mutation_Refreshes_List_Panels() { var firstItem = CreateListItem("first"); var secondItem = CreateListItem("second"); var targetKeys = new ObservableCollection { "first" }; var transfer = new ListTransfer { Width = 520, Height = 260, ItemsSource = new[] { firstItem, secondItem }, TargetKeys = targetKeys }; ShowInWindow(transfer, () => { var sourceView = transfer.SourceView.ShouldBeOfType(); var targetView = transfer.TargetView.ShouldBeOfType(); sourceView.Items.Cast().Select(item => item.ItemKey).ShouldBe([secondItem.ItemKey]); targetView.Items.Cast().Select(item => item.ItemKey).ShouldBe([firstItem.ItemKey]); targetKeys.Add("second"); Dispatcher.UIThread.RunJobs(); sourceView.Items.Cast().ShouldBeEmpty(); targetView.Items.Cast() .Select(item => item.ItemKey) .ShouldBe([firstItem.ItemKey, secondItem.ItemKey]); targetKeys.Remove("first"); Dispatcher.UIThread.RunJobs(); sourceView.Items.Cast().Select(item => item.ItemKey).ShouldBe([firstItem.ItemKey]); targetView.Items.Cast().Select(item => item.ItemKey).ShouldBe([secondItem.ItemKey]); }); } [Fact] public void TargetKeys_ObservableCollection_Mutation_Refreshes_Tree_Mask() { var child = new TreeItemNode { ItemKey = "child", Header = "Child" }; var parent = new TreeItemNode { ItemKey = "parent", Header = "Parent" }; parent.Children.Add(child); var targetKeys = new ObservableCollection(); var transfer = new TreeTransfer { ItemsSource = new[] { parent }, TargetKeys = targetKeys, Width = 520, Height = 260 }; ShowInWindow(transfer, () => { var sourceView = transfer.SourceView.ShouldBeOfType(); sourceView.MaskKeys.ShouldBeEmpty(); targetKeys.Add("child"); Dispatcher.UIThread.RunJobs(); sourceView.MaskKeys.ShouldNotBeNull(); sourceView.MaskKeys.ShouldContain("child"); targetKeys.Clear(); Dispatcher.UIThread.RunJobs(); sourceView.MaskKeys.ShouldBeEmpty(); }); } [Fact] public void SelectedKeys_ObservableCollection_Mutation_Splits_Selection_Between_List_Panels() { var firstItem = CreateListItem("first"); var secondItem = CreateListItem("second"); var selectedKeys = new ObservableCollection { "first" }; var transfer = new ListTransfer { Width = 520, Height = 260, ItemsSource = new[] { firstItem, secondItem }, TargetKeys = new List { "second" }, SelectedKeys = selectedKeys }; ShowInWindow(transfer, () => { var sourceView = transfer.SourceView.ShouldBeOfType(); var targetView = transfer.TargetView.ShouldBeOfType(); sourceView.SelectedKeys.ShouldBe(["first"]); targetView.SelectedKeys.ShouldBeNull(); selectedKeys.Add("second"); Dispatcher.UIThread.RunJobs(); sourceView.SelectedKeys.ShouldBe(["first"]); targetView.SelectedKeys.ShouldBe(["second"]); selectedKeys.Clear(); Dispatcher.UIThread.RunJobs(); sourceView.SelectedKeys.ShouldBeNull(); targetView.SelectedKeys.ShouldBeNull(); }); } [Fact] public void Clearing_TargetKeys_Keeps_Equivalent_Source_Selection_Stable() { var items = Enumerable.Range(1, 6) .Select(index => CreateListItem(index.ToString())) .ToList(); var targetKeys = new ObservableCollection { "3" }; var selectedKeys = new ObservableCollection { "2", "4" }; var transfer = new ListTransfer { Width = 520, Height = 260, ItemsSource = items, TargetKeys = targetKeys, SelectedKeys = selectedKeys }; ShowInWindow(transfer, () => { var sourceView = transfer.SourceView.ShouldBeOfType(); sourceView.SelectedKeys.ShouldBe(["2", "4"]); sourceView.SelectedItems.ShouldNotBeNull(); sourceView.SelectedItems .Cast() .Select(item => item.ItemKey) .ShouldBe(["2", "4"]); var originalSourceSelectedKeys = sourceView.SelectedKeys; var selectedKeysChangeCount = 0; var selectedItemsChangeCount = 0; var observedEmptySelection = false; sourceView.PropertyChanged += (_, args) => { if (args.Property == TransferListView.SelectedKeysProperty) { selectedKeysChangeCount++; } else if (args.Property == global::AtomUI.Desktop.Controls.ListView.SelectedItemsProperty) { selectedItemsChangeCount++; observedEmptySelection |= sourceView.SelectedItems == null || sourceView.SelectedItems.Count == 0; } }; targetKeys.Clear(); Dispatcher.UIThread.RunJobs(); sourceView.SelectedKeys.ShouldBeSameAs(originalSourceSelectedKeys); sourceView.SelectedKeys.ShouldBe(["2", "4"]); sourceView.SelectedItems .Cast() .Select(item => item.ItemKey) .ShouldBe(["2", "4"]); selectedKeysChangeCount.ShouldBe(0); selectedItemsChangeCount.ShouldBe(0); observedEmptySelection.ShouldBeFalse(); }); } [Fact] public void Clearing_Empty_TargetKeys_Does_Not_Rebuild_List_Panel_Sources() { var items = Enumerable.Range(1, 6) .Select(index => CreateListItem(index.ToString())) .ToList(); var targetKeys = new ObservableCollection(); var selectedKeys = new ObservableCollection { "2", "4" }; var transfer = new ListTransfer { Width = 520, Height = 260, ItemsSource = items, TargetKeys = targetKeys, SelectedKeys = selectedKeys }; ShowInWindow(transfer, () => { var sourceView = transfer.SourceView.ShouldBeOfType(); var originalSourceViewSource = transfer.SourceViewSource; var originalTargetViewSource = transfer.TargetViewSource; var originalSourceItemsSource = sourceView.ItemsSource; var sourceViewSourceChangeCount = 0; var targetViewSourceChangeCount = 0; var sourceItemsSourceChangeCount = 0; transfer.PropertyChanged += (_, args) => { if (args.Property == AbstractTransfer.SourceViewSourceProperty) { sourceViewSourceChangeCount++; } else if (args.Property == AbstractTransfer.TargetViewSourceProperty) { targetViewSourceChangeCount++; } }; sourceView.PropertyChanged += (_, args) => { if (args.Property == ItemsControl.ItemsSourceProperty) { sourceItemsSourceChangeCount++; } }; targetKeys.Clear(); Dispatcher.UIThread.RunJobs(); transfer.SourceViewSource.ShouldBeSameAs(originalSourceViewSource); transfer.TargetViewSource.ShouldBeSameAs(originalTargetViewSource); sourceView.ItemsSource.ShouldBeSameAs(originalSourceItemsSource); sourceViewSourceChangeCount.ShouldBe(0); targetViewSourceChangeCount.ShouldBe(0); sourceItemsSourceChangeCount.ShouldBe(0); }); } [Fact] public void Clearing_TargetKeys_Updates_List_Panel_Sources_In_Place() { var items = Enumerable.Range(1, 6) .Select(index => CreateListItem(index.ToString())) .ToList(); var targetKeys = new ObservableCollection { "1", "3" }; var selectedKeys = new ObservableCollection { "2", "4" }; var transfer = new ListTransfer { Width = 520, Height = 260, ItemsSource = items, TargetKeys = targetKeys, SelectedKeys = selectedKeys }; ShowInWindow(transfer, () => { var sourceView = transfer.SourceView.ShouldBeOfType(); var targetView = transfer.TargetView.ShouldBeOfType(); var originalSourceViewSource = transfer.SourceViewSource; var originalTargetViewSource = transfer.TargetViewSource; var originalSourceItemsSource = sourceView.ItemsSource; var originalTargetItemsSource = targetView.ItemsSource; var sourceViewSourceChangeCount = 0; var targetViewSourceChangeCount = 0; var sourceItemsSourceChangeCount = 0; var targetItemsSourceChangeCount = 0; transfer.PropertyChanged += (_, args) => { if (args.Property == AbstractTransfer.SourceViewSourceProperty) { sourceViewSourceChangeCount++; } else if (args.Property == AbstractTransfer.TargetViewSourceProperty) { targetViewSourceChangeCount++; } }; sourceView.PropertyChanged += (_, args) => { if (args.Property == ItemsControl.ItemsSourceProperty) { sourceItemsSourceChangeCount++; } }; targetView.PropertyChanged += (_, args) => { if (args.Property == ItemsControl.ItemsSourceProperty) { targetItemsSourceChangeCount++; } }; targetKeys.Clear(); Dispatcher.UIThread.RunJobs(); transfer.SourceViewSource.ShouldBeSameAs(originalSourceViewSource); transfer.TargetViewSource.ShouldBeSameAs(originalTargetViewSource); sourceView.ItemsSource.ShouldBeSameAs(originalSourceItemsSource); targetView.ItemsSource.ShouldBeSameAs(originalTargetItemsSource); sourceViewSourceChangeCount.ShouldBe(0); targetViewSourceChangeCount.ShouldBe(0); sourceItemsSourceChangeCount.ShouldBe(0); targetItemsSourceChangeCount.ShouldBe(0); sourceView.Items.Cast() .Select(item => item.ItemKey) .ShouldBe(["1", "2", "3", "4", "5", "6"]); targetView.Items.Cast().ShouldBeEmpty(); sourceView.SelectedKeys.ShouldBe(["2", "4"]); }); } [Fact] public void Clearing_TargetKeys_Does_Not_Crash_When_List_Panels_Recycle_Containers() { var items = Enumerable.Range(1, 64) .Select(index => CreateListItem(index.ToString())) .ToList(); var targetKeys = new ObservableCollection( Enumerable.Range(1, 32).Select(index => (EntityKey)index.ToString())); var selectedKeys = new ObservableCollection { "33", "35" }; var transfer = new ListTransfer { Width = 520, Height = 220, ItemsSource = items, TargetKeys = targetKeys, SelectedKeys = selectedKeys }; ShowInWindow(transfer, () => { targetKeys.Clear(); Dispatcher.UIThread.RunJobs(); Dispatcher.UIThread.RunJobs(); transfer.TargetKeys.ShouldBeEmpty(); transfer.SourceView.ShouldBeOfType() .SelectedKeys.ShouldBe(["33", "35"]); }); } [Fact] public void Transfer_To_Target_Updates_Existing_TargetKeys_Collection() { var firstItem = CreateListItem("first"); var secondItem = CreateListItem("second"); var targetKeys = new ObservableCollection { "first" }; var transfer = new ListTransfer { Width = 520, Height = 260, ItemsSource = new[] { firstItem, secondItem }, TargetKeys = targetKeys }; ShowInWindow(transfer, () => { var sourceView = transfer.SourceView.ShouldBeOfType(); sourceView.SelectedKeys = new List { "second" }; Dispatcher.UIThread.RunJobs(); TransferForTest(transfer, TransferDirection.ToTarget); Dispatcher.UIThread.RunJobs(); transfer.TargetKeys.ShouldBeSameAs(targetKeys); targetKeys.ShouldBe(["first", "second"]); transfer.SelectedKeys.ShouldBeEmpty(); }); } [Fact] public void RemoveAll_Clears_Existing_TargetKeys_Collection() { var firstItem = CreateListItem("first"); var secondItem = CreateListItem("second"); var targetKeys = new ObservableCollection { "first", "second" }; var transfer = new ListTransfer { Width = 520, Height = 260, ItemsSource = new[] { firstItem, secondItem }, TargetKeys = targetKeys }; ShowInWindow(transfer, () => { transfer.RaiseEvent(new TransferSelectActionEventArgs(TransferSelectAction.RemoveAll) { RoutedEvent = TransferSelectDropdown.SelectActionRequestEvent, Source = transfer }); Dispatcher.UIThread.RunJobs(); transfer.TargetKeys.ShouldBeSameAs(targetKeys); targetKeys.ShouldBeEmpty(); }); } [Fact] public void TransferListView_SelectedKeys_ObservableCollection_Mutation_Refreshes_SelectedItems() { var firstItem = CreateListItem("first"); var secondItem = CreateListItem("second"); var selectedKeys = new ObservableCollection { "first" }; var view = new TransferListView { ItemsSource = new[] { firstItem, secondItem }, SelectedKeys = selectedKeys }; ShowInWindow(view, () => { view.SelectedItems.ShouldNotBeNull(); view.SelectedItems.Cast().ShouldBe([firstItem]); selectedKeys.Add("second"); Dispatcher.UIThread.RunJobs(); view.SelectedItems.Cast().ShouldBe([firstItem, secondItem]); selectedKeys.Clear(); Dispatcher.UIThread.RunJobs(); view.SelectedItems.Count.ShouldBe(0); }); } [Fact] public void TransferTreeView_SelectedKeys_ObservableCollection_Mutation_Refreshes_CheckedItems() { var firstNode = new TreeItemNode { ItemKey = "first", Header = "First" }; var secondNode = new TreeItemNode { ItemKey = "second", Header = "Second" }; var selectedKeys = new ObservableCollection { "first" }; var view = new TransferTreeView { ItemsSource = new[] { firstNode, secondNode }, SelectedKeys = selectedKeys }; var selectionCount = 0; view.SelectionCountChanged += (_, args) => selectionCount = args.Count; ShowInWindow(view, () => { view.SelectedKeys.ShouldBeSameAs(selectedKeys); view.CheckedItems.Cast().ShouldBe([firstNode]); selectedKeys.Add("second"); Dispatcher.UIThread.RunJobs(); selectionCount.ShouldBe(2); view.CheckedItems .Cast() .Select(item => item.ItemKey) .ShouldBe(["first", "second"]); view.CheckedItems.Count.ShouldBe(2); view.CheckedItems.Cast().ShouldContain(firstNode); view.CheckedItems.Cast().ShouldContain(secondNode); selectedKeys.Clear(); Dispatcher.UIThread.RunJobs(); view.CheckedItems.Count.ShouldBe(0); }); } private static ListItemData CreateListItem(string key) { return new ListItemData { ItemKey = key, Content = key }; } private static void ShowInWindow(Control content, Action assertion) { var window = new AvaloniaWindow { Width = 640, Height = 360, Content = content }; try { window.Show(); Dispatcher.UIThread.RunJobs(); assertion(); } finally { window.Close(); } } private static void TransferForTest(AbstractTransfer transfer, TransferDirection transferDirection) { var method = typeof(AbstractTransfer).GetMethod( "TransferItems", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); method.ShouldNotBeNull(); method.Invoke(transfer, [transferDirection]); } }