mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-20 15:52:17 +08:00
538 lines
19 KiB
C#
538 lines
19 KiB
C#
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<EntityKey> { "first" };
|
|
var transfer = new ListTransfer
|
|
{
|
|
Width = 520,
|
|
Height = 260,
|
|
ItemsSource = new[] { firstItem, secondItem },
|
|
TargetKeys = targetKeys
|
|
};
|
|
|
|
ShowInWindow(transfer, () =>
|
|
{
|
|
var sourceView = transfer.SourceView.ShouldBeOfType<TransferListView>();
|
|
var targetView = transfer.TargetView.ShouldBeOfType<TransferListView>();
|
|
sourceView.Items.Cast<IItemKey>().Select(item => item.ItemKey).ShouldBe([secondItem.ItemKey]);
|
|
targetView.Items.Cast<IItemKey>().Select(item => item.ItemKey).ShouldBe([firstItem.ItemKey]);
|
|
|
|
targetKeys.Add("second");
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
sourceView.Items.Cast<IItemKey>().ShouldBeEmpty();
|
|
targetView.Items.Cast<IItemKey>()
|
|
.Select(item => item.ItemKey)
|
|
.ShouldBe([firstItem.ItemKey, secondItem.ItemKey]);
|
|
|
|
targetKeys.Remove("first");
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
sourceView.Items.Cast<IItemKey>().Select(item => item.ItemKey).ShouldBe([firstItem.ItemKey]);
|
|
targetView.Items.Cast<IItemKey>().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<EntityKey>();
|
|
var transfer = new TreeTransfer
|
|
{
|
|
ItemsSource = new[] { parent },
|
|
TargetKeys = targetKeys,
|
|
Width = 520,
|
|
Height = 260
|
|
};
|
|
|
|
ShowInWindow(transfer, () =>
|
|
{
|
|
var sourceView = transfer.SourceView.ShouldBeOfType<TransferTreeView>();
|
|
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<EntityKey> { "first" };
|
|
var transfer = new ListTransfer
|
|
{
|
|
Width = 520,
|
|
Height = 260,
|
|
ItemsSource = new[] { firstItem, secondItem },
|
|
TargetKeys = new List<EntityKey> { "second" },
|
|
SelectedKeys = selectedKeys
|
|
};
|
|
|
|
ShowInWindow(transfer, () =>
|
|
{
|
|
var sourceView = transfer.SourceView.ShouldBeOfType<TransferListView>();
|
|
var targetView = transfer.TargetView.ShouldBeOfType<TransferListView>();
|
|
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<EntityKey> { "3" };
|
|
var selectedKeys = new ObservableCollection<EntityKey> { "2", "4" };
|
|
var transfer = new ListTransfer
|
|
{
|
|
Width = 520,
|
|
Height = 260,
|
|
ItemsSource = items,
|
|
TargetKeys = targetKeys,
|
|
SelectedKeys = selectedKeys
|
|
};
|
|
|
|
ShowInWindow(transfer, () =>
|
|
{
|
|
var sourceView = transfer.SourceView.ShouldBeOfType<TransferListView>();
|
|
sourceView.SelectedKeys.ShouldBe(["2", "4"]);
|
|
sourceView.SelectedItems.ShouldNotBeNull();
|
|
sourceView.SelectedItems
|
|
.Cast<IItemKey>()
|
|
.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<IItemKey>()
|
|
.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<EntityKey>();
|
|
var selectedKeys = new ObservableCollection<EntityKey> { "2", "4" };
|
|
var transfer = new ListTransfer
|
|
{
|
|
Width = 520,
|
|
Height = 260,
|
|
ItemsSource = items,
|
|
TargetKeys = targetKeys,
|
|
SelectedKeys = selectedKeys
|
|
};
|
|
|
|
ShowInWindow(transfer, () =>
|
|
{
|
|
var sourceView = transfer.SourceView.ShouldBeOfType<TransferListView>();
|
|
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<EntityKey> { "1", "3" };
|
|
var selectedKeys = new ObservableCollection<EntityKey> { "2", "4" };
|
|
var transfer = new ListTransfer
|
|
{
|
|
Width = 520,
|
|
Height = 260,
|
|
ItemsSource = items,
|
|
TargetKeys = targetKeys,
|
|
SelectedKeys = selectedKeys
|
|
};
|
|
|
|
ShowInWindow(transfer, () =>
|
|
{
|
|
var sourceView = transfer.SourceView.ShouldBeOfType<TransferListView>();
|
|
var targetView = transfer.TargetView.ShouldBeOfType<TransferListView>();
|
|
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<IItemKey>()
|
|
.Select(item => item.ItemKey)
|
|
.ShouldBe(["1", "2", "3", "4", "5", "6"]);
|
|
targetView.Items.Cast<IItemKey>().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<EntityKey>(
|
|
Enumerable.Range(1, 32).Select(index => (EntityKey)index.ToString()));
|
|
var selectedKeys = new ObservableCollection<EntityKey> { "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<TransferListView>()
|
|
.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<EntityKey> { "first" };
|
|
var transfer = new ListTransfer
|
|
{
|
|
Width = 520,
|
|
Height = 260,
|
|
ItemsSource = new[] { firstItem, secondItem },
|
|
TargetKeys = targetKeys
|
|
};
|
|
|
|
ShowInWindow(transfer, () =>
|
|
{
|
|
var sourceView = transfer.SourceView.ShouldBeOfType<TransferListView>();
|
|
sourceView.SelectedKeys = new List<EntityKey> { "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<EntityKey> { "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<EntityKey> { "first" };
|
|
var view = new TransferListView
|
|
{
|
|
ItemsSource = new[] { firstItem, secondItem },
|
|
SelectedKeys = selectedKeys
|
|
};
|
|
|
|
ShowInWindow(view, () =>
|
|
{
|
|
view.SelectedItems.ShouldNotBeNull();
|
|
view.SelectedItems.Cast<IListItemData>().ShouldBe([firstItem]);
|
|
|
|
selectedKeys.Add("second");
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
view.SelectedItems.Cast<IListItemData>().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<EntityKey> { "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<ITreeItemNode>().ShouldBe([firstNode]);
|
|
|
|
selectedKeys.Add("second");
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
selectionCount.ShouldBe(2);
|
|
view.CheckedItems
|
|
.Cast<ITreeItemNode>()
|
|
.Select(item => item.ItemKey)
|
|
.ShouldBe(["first", "second"]);
|
|
view.CheckedItems.Count.ShouldBe(2);
|
|
view.CheckedItems.Cast<ITreeItemNode>().ShouldContain(firstNode);
|
|
view.CheckedItems.Cast<ITreeItemNode>().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]);
|
|
}
|
|
}
|