mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-18 22:46:38 +08:00
1678 lines
56 KiB
C#
1678 lines
56 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.Reflection;
|
|
using AtomUI.Controls;
|
|
using AtomUI.Controls.Utils;
|
|
using AtomUI.Controls.Primitives;
|
|
using AtomUI.Desktop.Controls.DesignTokens;
|
|
using AtomUI.Icons.AntDesign;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Presenters;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Data;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Media;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.SelectControl;
|
|
|
|
public class SelectBehaviorTests
|
|
{
|
|
static SelectBehaviorTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void Pinned_Open_Request_Opens_Select_And_Its_Template_Popup()
|
|
{
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
IsMotionEnabled = false,
|
|
OptionsSource = [new SelectOption { Header = "Jack", Content = "jack" }]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsPopupPinnedOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var popup = GetVisualDescendant<Popup>(select, "PART_Popup");
|
|
select.IsDropDownOpen.ShouldBeTrue();
|
|
popup.IsPopupPinnedOpen.ShouldBeTrue();
|
|
popup.IsOpen.ShouldBeTrue();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Pinned_Select_Rejects_Business_Close_Request()
|
|
{
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
IsMotionEnabled = false,
|
|
OptionsSource = [new SelectOption { Header = "Jack", Content = "jack" }]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsPopupPinnedOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
var popup = GetVisualDescendant<Popup>(select, "PART_Popup");
|
|
|
|
select.IsDropDownOpen = false;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.IsDropDownOpen.ShouldBeTrue();
|
|
popup.IsPopupPinnedOpen.ShouldBeTrue();
|
|
popup.IsOpen.ShouldBeTrue();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Pinned_Select_Detach_Closes_Popup_And_Reattach_Reopens_It()
|
|
{
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
IsMotionEnabled = false,
|
|
OptionsSource = [new SelectOption { Header = "Jack", Content = "jack" }]
|
|
};
|
|
var window = CreateWindow(select);
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
var visualLayerManager = window.Content.ShouldBeOfType<VisualLayerManager>();
|
|
var overlayPanel = visualLayerManager.Child.ShouldBeOfType<ScopeAwareOverlayLayerPanel>();
|
|
|
|
select.IsPopupPinnedOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
var popup = GetVisualDescendant<Popup>(select, "PART_Popup");
|
|
popup.IsOpen.ShouldBeTrue();
|
|
|
|
overlayPanel.Children.Remove(select);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.IsPopupPinnedOpen.ShouldBeTrue();
|
|
popup.IsOpen.ShouldBeFalse();
|
|
window.GetVisualDescendants().OfType<OverlayPopupHost>().ShouldBeEmpty();
|
|
|
|
overlayPanel.Children.Add(select);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.IsDropDownOpen.ShouldBeTrue();
|
|
popup.IsOpen.ShouldBeTrue(
|
|
$"selectAttached={select.IsAttachedToVisualTree()}, popupAttached={popup.IsAttachedToVisualTree()}, " +
|
|
$"popupPinned={popup.IsPopupPinnedOpen}, child={popup.Child is not null}, " +
|
|
$"placementTarget={popup.PlacementTarget?.GetType().Name ?? "null"}, " +
|
|
$"suspended={GetPrivateField<bool>(popup, "_isPinnedOpenSuspended")}, " +
|
|
$"trackedTarget={GetPrivateField<Control?>(popup, "_pinnedOpenTrackingTarget")?.GetType().Name ?? "null"}");
|
|
window.GetVisualDescendants().OfType<OverlayPopupHost>().ShouldHaveSingleItem();
|
|
}
|
|
finally
|
|
{
|
|
select.IsPopupPinnedOpen = false;
|
|
window.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Pinned_Select_Template_Replacement_Closes_Old_Popup_And_Opens_New_Popup()
|
|
{
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
IsMotionEnabled = false,
|
|
OptionsSource = [new SelectOption { Header = "Jack", Content = "jack" }]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsPopupPinnedOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
var oldPopup = GetVisualDescendant<Popup>(select, "PART_Popup");
|
|
var template = select.Template;
|
|
template.ShouldNotBeNull();
|
|
|
|
select.Template = null;
|
|
select.ApplyTemplate();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
oldPopup.IsOpen.ShouldBeFalse();
|
|
|
|
select.Template = template;
|
|
select.ApplyTemplate();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var newPopup = GetVisualDescendant<Popup>(select, "PART_Popup");
|
|
newPopup.ShouldNotBeSameAs(oldPopup);
|
|
select.IsDropDownOpen.ShouldBeTrue();
|
|
newPopup.IsPopupPinnedOpen.ShouldBeTrue();
|
|
newPopup.IsOpen.ShouldBeTrue();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void OptionsSource_Replacement_Preserves_Single_Selection_By_Content()
|
|
{
|
|
var firstLucy = new SelectOption
|
|
{
|
|
Header = "Lucy",
|
|
Content = "lucy"
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
OptionsSource = [new SelectOption { Header = "Jack", Content = "jack" }, firstLucy],
|
|
SelectedOption = firstLucy
|
|
};
|
|
var secondLucy = new SelectOption
|
|
{
|
|
Header = "Lucy Updated",
|
|
Content = "lucy"
|
|
};
|
|
|
|
select.OptionsSource =
|
|
[
|
|
new SelectOption { Header = "Jack Updated", Content = "jack" },
|
|
secondLucy
|
|
];
|
|
|
|
select.SelectedOption.ShouldBeSameAs(secondLucy);
|
|
}
|
|
|
|
[Fact]
|
|
public void OptionsSource_Replacement_Refreshes_Localized_Multiple_Tag_Layout()
|
|
{
|
|
var firstOption = new SelectOption
|
|
{
|
|
Header = "A",
|
|
Content = "a"
|
|
};
|
|
var secondOption = new SelectOption
|
|
{
|
|
Header = "B",
|
|
Content = "b"
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = SelectMode.Multiple,
|
|
IsFilterEnabled = true,
|
|
IsResponsiveTagMode = true,
|
|
MaxTagCount = 1,
|
|
OptionsSource = [firstOption, secondOption],
|
|
SelectedOptions = [firstOption, secondOption]
|
|
};
|
|
var firstLocalizedOption = new SelectOption
|
|
{
|
|
Header = "A much longer localized option",
|
|
Content = "a"
|
|
};
|
|
var secondLocalizedOption = new SelectOption
|
|
{
|
|
Header = "B much longer localized option",
|
|
Content = "b"
|
|
};
|
|
var firstLocalizedHeader = (string)firstLocalizedOption.Header!;
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
var selectedOptionsBox = GetVisualDescendant<SelectResultOptionsBox>(select, "SelectedOptionsBox");
|
|
var maxCountPanel = GetVisualDescendant<SelectMaxTagAwarePanel>(select, "PART_MaxCountAwarePanel");
|
|
|
|
select.OptionsSource = [firstLocalizedOption, secondLocalizedOption];
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.SelectedOptions.ShouldBe([firstLocalizedOption, secondLocalizedOption]);
|
|
selectedOptionsBox.GetVisualDescendants()
|
|
.OfType<SelectTag>()
|
|
.ShouldContain(tag => tag.Text == firstLocalizedHeader);
|
|
|
|
var infoTag = maxCountPanel.Children
|
|
.OfType<SelectRemainInfoTag>()
|
|
.Single();
|
|
var searchTextBox = GetTagsSearchTextBox(select);
|
|
|
|
infoTag.IsVisible.ShouldBeTrue();
|
|
infoTag.Bounds.Right.ShouldBeLessThanOrEqualTo(maxCountPanel.Bounds.Right + 0.001);
|
|
searchTextBox.Bounds.X.ShouldBeGreaterThanOrEqualTo(infoTag.Bounds.Right - 0.001);
|
|
searchTextBox.Bounds.Right.ShouldBeLessThanOrEqualTo(maxCountPanel.Bounds.Right + 0.001);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DefaultValues_Selects_Single_Option_On_Load()
|
|
{
|
|
var lucy = new SelectOption
|
|
{
|
|
Header = "Lucy",
|
|
Content = "lucy"
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
OptionsSource =
|
|
[
|
|
new SelectOption { Header = "Jack", Content = "jack" },
|
|
lucy
|
|
],
|
|
DefaultValues = ["lucy"]
|
|
};
|
|
|
|
ShowInWindow(select, () => select.SelectedOption.ShouldBeSameAs(lucy));
|
|
}
|
|
|
|
[Fact]
|
|
public void SelectedOption_DefaultBinding_Updates_ViewModel_When_Control_Value_Changes()
|
|
{
|
|
var jack = new SelectOption
|
|
{
|
|
Header = "Jack",
|
|
Content = "jack"
|
|
};
|
|
var lucy = new SelectOption
|
|
{
|
|
Header = "Lucy",
|
|
Content = "lucy"
|
|
};
|
|
var viewModel = new SelectBindingViewModel
|
|
{
|
|
SelectedOption = jack
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
OptionsSource = [jack, lucy]
|
|
};
|
|
select.Bind(
|
|
Desktop.Controls.Select.SelectedOptionProperty,
|
|
new Binding(nameof(SelectBindingViewModel.SelectedOption))
|
|
{
|
|
Source = viewModel
|
|
});
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.SelectedOption.ShouldBeSameAs(jack);
|
|
|
|
select.SelectedOption = lucy;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
viewModel.SelectedOption.ShouldBeSameAs(lucy);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void SelectedOptions_DefaultBinding_Updates_ViewModel_When_Control_Value_Changes()
|
|
{
|
|
var jack = new SelectOption
|
|
{
|
|
Header = "Jack",
|
|
Content = "jack"
|
|
};
|
|
var lucy = new SelectOption
|
|
{
|
|
Header = "Lucy",
|
|
Content = "lucy"
|
|
};
|
|
var viewModel = new SelectBindingViewModel();
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Mode = SelectMode.Multiple,
|
|
OptionsSource = [jack, lucy]
|
|
};
|
|
select.Bind(
|
|
Desktop.Controls.Select.SelectedOptionsProperty,
|
|
new Binding(nameof(SelectBindingViewModel.SelectedOptions))
|
|
{
|
|
Source = viewModel
|
|
});
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.SelectedOptions = [jack, lucy];
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
viewModel.SelectedOptions.ShouldNotBeNull();
|
|
viewModel.SelectedOptions.ShouldBe([jack, lucy]);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void SelectedOptions_ObservableCollection_Mutation_Refreshes_Result_And_Candidate_Selection()
|
|
{
|
|
var jack = new SelectOption
|
|
{
|
|
Header = "Jack",
|
|
Content = "jack"
|
|
};
|
|
var lucy = new SelectOption
|
|
{
|
|
Header = "Lucy",
|
|
Content = "lucy"
|
|
};
|
|
var viewModel = new SelectBindingViewModel
|
|
{
|
|
SelectedOptions = new ObservableCollection<ISelectOption>
|
|
{
|
|
jack
|
|
}
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = SelectMode.Multiple,
|
|
IsFilterEnabled = false,
|
|
OptionsSource = [jack, lucy]
|
|
};
|
|
select.Bind(
|
|
Desktop.Controls.Select.SelectedOptionsProperty,
|
|
new Binding(nameof(SelectBindingViewModel.SelectedOptions))
|
|
{
|
|
Source = viewModel
|
|
});
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.GetVisualDescendants()
|
|
.OfType<SelectTag>()
|
|
.Single(tag => tag.Text == "Jack");
|
|
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var candidateList = GetCandidateList(select);
|
|
candidateList.SelectedItems.ShouldNotBeNull();
|
|
candidateList.SelectedItems.Cast<ISelectOption>().ShouldContain(jack);
|
|
|
|
viewModel.SelectedOptions!.Add(lucy);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.GetVisualDescendants()
|
|
.OfType<SelectTag>()
|
|
.Single(tag => tag.Text == "Lucy");
|
|
candidateList.SelectedItems.Cast<ISelectOption>().ShouldContain(lucy);
|
|
|
|
viewModel.SelectedOptions.Remove(jack);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.GetVisualDescendants()
|
|
.OfType<SelectTag>()
|
|
.ShouldNotContain(tag => tag.Text == "Jack");
|
|
candidateList.SelectedItems.Cast<ISelectOption>().ShouldNotContain(jack);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Form_Value_Uses_Mode_Specific_Selection_Property()
|
|
{
|
|
var jack = new SelectOption
|
|
{
|
|
Header = "Jack",
|
|
Content = "jack"
|
|
};
|
|
var lucy = new SelectOption
|
|
{
|
|
Header = "Lucy",
|
|
Content = "lucy"
|
|
};
|
|
var select = new Desktop.Controls.Select();
|
|
var formItem = (IFormItemAware)select;
|
|
|
|
formItem.SetFormValue(jack);
|
|
formItem.GetFormValue().ShouldBeSameAs(jack);
|
|
select.SelectedOption.ShouldBeSameAs(jack);
|
|
|
|
select.Mode = SelectMode.Multiple;
|
|
IList<ISelectOption> selectedOptions = [jack, lucy];
|
|
|
|
formItem.SetFormValue(selectedOptions);
|
|
formItem.GetFormValue().ShouldBeSameAs(selectedOptions);
|
|
select.SelectedOptions.ShouldBeSameAs(selectedOptions);
|
|
|
|
formItem.ClearFormValue();
|
|
|
|
select.SelectedOptions.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Template_Binds_Right_AddOn_Count_And_Handle_State_From_Axaml()
|
|
{
|
|
var jack = new SelectOption
|
|
{
|
|
Header = "Jack",
|
|
Content = "jack"
|
|
};
|
|
var lucy = new SelectOption
|
|
{
|
|
Header = "Lucy",
|
|
Content = "lucy"
|
|
};
|
|
var rightAddOn = new TextBlock { Text = "extra" };
|
|
var suffixIcon = new DownOutlined();
|
|
var loadingIcon = new LoadingOutlined();
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = SelectMode.Multiple,
|
|
IsFilterEnabled = true,
|
|
IsAllowClear = true,
|
|
IsMotionEnabled = false,
|
|
MaxCount = 3,
|
|
IsShowMaxCountIndicator = true,
|
|
SelectedOptions = [jack, lucy],
|
|
ContentRightAddOn = rightAddOn,
|
|
SuffixIcon = suffixIcon,
|
|
SuffixLoadingIcon = loadingIcon
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
var indicator = GetVisualDescendant<SelectMaxCountIndicator>(select, "PART_SelectMaxCountIndicator");
|
|
indicator.MaxCount.ShouldBe(3);
|
|
indicator.SelectedCount.ShouldBe(2);
|
|
indicator.IsVisible.ShouldBeTrue();
|
|
|
|
var contentPresenter = GetVisualDescendant<ContentPresenter>(select, "PART_ContentRightAddOnPresenter");
|
|
contentPresenter.Content.ShouldBeSameAs(rightAddOn);
|
|
contentPresenter.IsVisible.ShouldBeTrue();
|
|
|
|
var handle = GetVisualDescendant<SelectHandle>(select, "PART_SelectHandle");
|
|
handle.OpenIndicator.ShouldBeSameAs(suffixIcon);
|
|
handle.LoadingIcon.ShouldBeSameAs(loadingIcon);
|
|
handle.IsFilterEnabled.ShouldBeTrue();
|
|
handle.IsMotionEnabled.ShouldBeFalse();
|
|
handle.IsAllowClear.ShouldBeTrue();
|
|
handle.IsSelectionEmpty.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Template_Relays_AddOnDecoratedBox_Input_State_To_Handle()
|
|
{
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
var addOnBox = GetVisualDescendant<AddOnDecoratedBox>(
|
|
select,
|
|
AddOnDecoratedBox.AddOnDecoratedBoxPart);
|
|
var handle = GetVisualDescendant<SelectHandle>(select, "PART_SelectHandle");
|
|
|
|
addOnBox.IsInnerBoxHover = true;
|
|
addOnBox.IsInnerBoxPressed = true;
|
|
|
|
handle.IsInputHover.ShouldBeTrue();
|
|
handle.IsInputPressed.ShouldBeTrue();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Custom_Size_Single_Selected_Text_Is_Vertically_Centered()
|
|
{
|
|
var jack = new SelectOption
|
|
{
|
|
Header = "Jack",
|
|
Content = "jack"
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Height = 38,
|
|
FontSize = 15,
|
|
SizeType = CustomizableSizeType.Custom,
|
|
IsAllowClear = true,
|
|
OptionsSource = [jack],
|
|
SelectedOption = jack,
|
|
PlaceholderText = "Please select"
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
var addOnBox = GetVisualDescendant<AddOnDecoratedBox>(
|
|
select,
|
|
AddOnDecoratedBox.AddOnDecoratedBoxPart);
|
|
var textPresenter = GetVisualDescendant<TextPresenter>(select, "PART_TextPresenter");
|
|
|
|
var textTop = textPresenter.TranslatePoint(default, addOnBox);
|
|
textTop.ShouldNotBeNull();
|
|
|
|
var textCenterY = textTop.Value.Y + textPresenter.Bounds.Height / 2;
|
|
var boxCenterY = addOnBox.Bounds.Height / 2;
|
|
|
|
Math.Abs(textCenterY - boxCenterY).ShouldBeLessThanOrEqualTo(1.0);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Single_Filter_Input_Uses_Outer_Content_Padding_Only()
|
|
{
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
IsFilterEnabled = true,
|
|
PlaceholderText = "Please select"
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
var addOnBox = GetVisualDescendant<AddOnDecoratedBox>(
|
|
select,
|
|
AddOnDecoratedBox.AddOnDecoratedBoxPart);
|
|
var searchTextBox = GetVisualDescendant<SelectFilterTextBox>(select, "PART_SingleFilterInput");
|
|
|
|
addOnBox.ContentFrame.ShouldNotBeNull();
|
|
addOnBox.ContentFrame!.Padding.Left.ShouldBeGreaterThan(0);
|
|
AssertEmbeddedSelectFilterTextBox(searchTextBox);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(SelectMode.Multiple)]
|
|
[InlineData(SelectMode.Tags)]
|
|
public void Multiple_And_Tags_Search_Input_Uses_Outer_Content_Padding_Only(SelectMode mode)
|
|
{
|
|
var jack = new SelectOption
|
|
{
|
|
Header = "Jack",
|
|
Content = "jack"
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = mode,
|
|
IsFilterEnabled = true,
|
|
SelectedOptions = [jack]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
var searchTextBox = GetTagsSearchTextBox(select);
|
|
|
|
AssertEmbeddedSelectFilterTextBox(searchTextBox);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(SelectMode.Multiple)]
|
|
[InlineData(SelectMode.Tags)]
|
|
public void Multiple_And_Tags_Placeholder_Hides_While_Search_Preedit_Text_Is_Rendered(SelectMode mode)
|
|
{
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = mode,
|
|
IsFilterEnabled = true,
|
|
PlaceholderText = "Please select"
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
var placeholder = GetVisualDescendant<Avalonia.Controls.TextBlock>(select, "PlaceholderText");
|
|
var searchTextBox = GetTagsSearchTextBox(select);
|
|
var presenter = GetVisualDescendant<TextPresenter>(searchTextBox, "PART_TextPresenter");
|
|
|
|
placeholder.IsVisible.ShouldBeTrue();
|
|
|
|
presenter.SetCurrentValue(TextPresenter.PreeditTextProperty, "测");
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
placeholder.IsVisible.ShouldBeFalse(
|
|
"IME preedit text is rendered by the Select search TextPresenter before Text is committed, so the outer placeholder must not remain over it.");
|
|
|
|
presenter.SetCurrentValue(TextPresenter.PreeditTextProperty, string.Empty);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
placeholder.IsVisible.ShouldBeTrue(
|
|
"The placeholder should return when search preedit text is cleared and the selection is still empty.");
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Custom_Size_Multiple_Tag_Uses_Input_Content_Height()
|
|
{
|
|
var jack = new SelectOption
|
|
{
|
|
Header = "Jack",
|
|
Content = "jack"
|
|
};
|
|
var lucy = new SelectOption
|
|
{
|
|
Header = "Lucy",
|
|
Content = "lucy"
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Height = 38,
|
|
Mode = SelectMode.Multiple,
|
|
SizeType = CustomizableSizeType.Custom,
|
|
IsFilterEnabled = false,
|
|
SelectedOptions = [jack, lucy]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
var addOnBox = GetVisualDescendant<AddOnDecoratedBox>(
|
|
select,
|
|
AddOnDecoratedBox.AddOnDecoratedBoxPart);
|
|
var tag = select.GetVisualDescendants()
|
|
.OfType<SelectTag>()
|
|
.First(item => item.Text == "Jack");
|
|
|
|
addOnBox.ContentFrame.ShouldNotBeNull();
|
|
var contentPadding = addOnBox.ContentFrame!.Padding;
|
|
var border = addOnBox.InnerBoxBorderThickness;
|
|
var expectedHeight = select.Height -
|
|
contentPadding.Top -
|
|
contentPadding.Bottom -
|
|
border.Top -
|
|
border.Bottom;
|
|
|
|
tag.Bounds.Height.ShouldBe(expectedHeight, 0.5);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Custom_Size_Multiple_Tag_Keeps_Minimum_Height_And_Uses_Small_Vertical_Padding_When_Input_Is_Low()
|
|
{
|
|
var jack = new SelectOption
|
|
{
|
|
Header = "Jack",
|
|
Content = "jack"
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Height = 28,
|
|
Mode = SelectMode.Multiple,
|
|
SizeType = CustomizableSizeType.Custom,
|
|
IsFilterEnabled = false,
|
|
SelectedOptions = [jack]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
var addOnBox = GetVisualDescendant<AddOnDecoratedBox>(
|
|
select,
|
|
AddOnDecoratedBox.AddOnDecoratedBoxPart);
|
|
var tag = select.GetVisualDescendants()
|
|
.OfType<SelectTag>()
|
|
.Single(item => item.Text == "Jack");
|
|
var minimumTagHeight = GetThemeResource<double>(SelectTokenKind.MultipleItemHeight);
|
|
var smallPadding = GetThemeResource<Thickness>(SelectTokenKind.MultiModePaddingSM);
|
|
|
|
addOnBox.ContentFrame.ShouldNotBeNull();
|
|
addOnBox.ContentFrame!.Padding.Top.ShouldBe(smallPadding.Top);
|
|
addOnBox.ContentFrame.Padding.Bottom.ShouldBe(smallPadding.Bottom);
|
|
tag.Bounds.Height.ShouldBe(minimumTagHeight, 0.5);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Committing_Current_Single_Selection_Does_Not_Suppress_Next_External_Selection_Sync()
|
|
{
|
|
var jack = new SelectOption
|
|
{
|
|
Header = "Jack",
|
|
Content = "jack"
|
|
};
|
|
var lucy = new SelectOption
|
|
{
|
|
Header = "Lucy",
|
|
Content = "lucy"
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
OptionsSource = [jack, lucy],
|
|
SelectedOption = jack
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var candidateList = GetCandidateList(select);
|
|
candidateList.SelectedItem.ShouldBeSameAs(jack);
|
|
|
|
candidateList.RaiseEvent(new RoutedEventArgs(SelectCandidateList.CommitEvent));
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.IsDropDownOpen.ShouldBeFalse();
|
|
|
|
select.SelectedOption = lucy;
|
|
|
|
candidateList.SelectedItem.ShouldBeSameAs(lucy);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Pointer_Move_And_Keyboard_Navigation_Share_One_Active_Candidate()
|
|
{
|
|
var jack = new SelectOption { Header = "Jack", Content = "jack" };
|
|
var lucy = new SelectOption { Header = "Lucy", Content = "lucy" };
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
IsMotionEnabled = false,
|
|
OptionsSource = [jack, lucy]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var candidateList = GetCandidateList(select);
|
|
var jackContainer = GetCandidateContainer(candidateList, 0);
|
|
var lucyContainer = GetCandidateContainer(candidateList, 1);
|
|
|
|
RaisePointerMoved(jackContainer);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
candidateList.CandidateSelectedIndex.ShouldBe(0);
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(jack);
|
|
jackContainer.IsCandidateSelected.ShouldBeTrue();
|
|
lucyContainer.IsCandidateSelected.ShouldBeFalse();
|
|
select.SelectedOption.ShouldBeNull();
|
|
|
|
RaiseCandidateKeyDown(candidateList, Key.Down);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
candidateList.CandidateSelectedIndex.ShouldBe(1);
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(lucy);
|
|
jackContainer.IsCandidateSelected.ShouldBeFalse();
|
|
lucyContainer.IsCandidateSelected.ShouldBeTrue();
|
|
select.SelectedOption.ShouldBeNull();
|
|
|
|
RaisePointerMoved(jackContainer);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
candidateList.CandidateSelectedIndex.ShouldBe(0);
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(jack);
|
|
jackContainer.IsCandidateSelected.ShouldBeTrue();
|
|
lucyContainer.IsCandidateSelected.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Candidate_Direct_Properties_Keep_Index_Item_And_Projection_Consistent()
|
|
{
|
|
var jack = new SelectOption { Header = "Jack", Content = "jack" };
|
|
var lucy = new SelectOption { Header = "Lucy", Content = "lucy" };
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
IsMotionEnabled = false,
|
|
OptionsSource = [jack, lucy]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var candidateList = GetCandidateList(select);
|
|
var jackContainer = GetCandidateContainer(candidateList, 0);
|
|
var lucyContainer = GetCandidateContainer(candidateList, 1);
|
|
|
|
candidateList.CandidateSelectedItem = lucy;
|
|
|
|
candidateList.CandidateSelectedIndex.ShouldBe(1);
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(lucy);
|
|
jackContainer.IsCandidateSelected.ShouldBeFalse();
|
|
lucyContainer.IsCandidateSelected.ShouldBeTrue();
|
|
|
|
candidateList.CandidateSelectedIndex = 0;
|
|
|
|
candidateList.CandidateSelectedIndex.ShouldBe(0);
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(jack);
|
|
jackContainer.IsCandidateSelected.ShouldBeTrue();
|
|
lucyContainer.IsCandidateSelected.ShouldBeFalse();
|
|
|
|
candidateList.CandidateSelectedItem = new SelectOption();
|
|
|
|
candidateList.CandidateSelectedIndex.ShouldBe(-1);
|
|
candidateList.CandidateSelectedItem.ShouldBeNull();
|
|
jackContainer.IsCandidateSelected.ShouldBeFalse();
|
|
lucyContainer.IsCandidateSelected.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Pointer_Move_Then_Enter_Commits_The_Pointer_Candidate()
|
|
{
|
|
var jack = new SelectOption { Header = "Jack", Content = "jack" };
|
|
var lucy = new SelectOption { Header = "Lucy", Content = "lucy" };
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
OptionsSource = [jack, lucy]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var candidateList = GetCandidateList(select);
|
|
RaiseCandidateKeyDown(candidateList, Key.Down);
|
|
RaisePointerMoved(GetCandidateContainer(candidateList, 1));
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(lucy);
|
|
|
|
RaiseCandidateKeyDown(candidateList, Key.Enter);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.SelectedOption.ShouldBeSameAs(lucy);
|
|
select.IsDropDownOpen.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void PointerOver_Does_Not_Preserve_A_Second_Candidate_Highlight()
|
|
{
|
|
var jack = new SelectOption { Header = "Jack", Content = "jack" };
|
|
var lucy = new SelectOption { Header = "Lucy", Content = "lucy" };
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
IsMotionEnabled = false,
|
|
OptionsSource = [jack, lucy]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var candidateList = GetCandidateList(select);
|
|
var jackContainer = GetCandidateContainer(candidateList, 0);
|
|
var lucyContainer = GetCandidateContainer(candidateList, 1);
|
|
var defaultBg = GetThemeResource<IBrush>(ListViewTokenKind.ItemBgColor);
|
|
var activeBg = GetThemeResource<IBrush>(SelectTokenKind.OptionActiveBg);
|
|
|
|
RaisePointerMoved(jackContainer);
|
|
((IPseudoClasses)jackContainer.Classes).Set(":pointerover", true);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
jackContainer.Background.ShouldBe(activeBg);
|
|
|
|
RaiseCandidateKeyDown(candidateList, Key.Down);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
jackContainer.IsCandidateSelected.ShouldBeFalse();
|
|
jackContainer.Background.ShouldBe(defaultBg);
|
|
lucyContainer.IsCandidateSelected.ShouldBeTrue();
|
|
lucyContainer.Background.ShouldBe(activeBg);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Committed_Selection_Visual_Overrides_Active_Candidate_Visual()
|
|
{
|
|
var jack = new SelectOption { Header = "Jack", Content = "jack" };
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
IsMotionEnabled = false,
|
|
OptionsSource = [jack],
|
|
SelectedOption = jack
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var candidateList = GetCandidateList(select);
|
|
var container = GetCandidateContainer(candidateList, 0);
|
|
var selectedBg = GetThemeResource<IBrush>(SelectTokenKind.OptionSelectedBg);
|
|
|
|
RaisePointerMoved(container);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(jack);
|
|
container.IsCandidateSelected.ShouldBeTrue();
|
|
container.IsSelected.ShouldBeTrue();
|
|
container.Background.ShouldBe(selectedBg);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Popup_Close_Filter_And_MaxCount_Invalidate_Active_Candidate()
|
|
{
|
|
var jack = new SelectOption { Header = "Jack", Content = "jack" };
|
|
var lucy = new SelectOption { Header = "Lucy", Content = "lucy" };
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = SelectMode.Multiple,
|
|
IsFilterEnabled = true,
|
|
Filter = ValueFilterFactory.BuildFilter(ValueFilterMode.Contains),
|
|
MaxCount = 2,
|
|
OptionsSource = [jack, lucy],
|
|
SelectedOptions = [jack]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var candidateList = GetCandidateList(select);
|
|
RaisePointerMoved(GetCandidateContainer(candidateList, 1));
|
|
Dispatcher.UIThread.RunJobs();
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(lucy);
|
|
|
|
select.MaxCount = 1;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
candidateList.CandidateSelectedIndex.ShouldBe(-1);
|
|
candidateList.CandidateSelectedItem.ShouldBeNull();
|
|
|
|
select.MaxCount = 2;
|
|
RaisePointerMoved(GetCandidateContainer(candidateList, 1));
|
|
Dispatcher.UIThread.RunJobs();
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(lucy);
|
|
|
|
select.FilterValue = "jack";
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
candidateList.CandidateSelectedIndex.ShouldBe(-1);
|
|
candidateList.CandidateSelectedItem.ShouldBeNull();
|
|
|
|
select.FilterValue = null;
|
|
Dispatcher.UIThread.RunJobs();
|
|
RaisePointerMoved(GetCandidateContainer(candidateList, 0));
|
|
Dispatcher.UIThread.RunJobs();
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(jack);
|
|
|
|
select.IsDropDownOpen = false;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
candidateList.CandidateSelectedIndex.ShouldBe(-1);
|
|
candidateList.CandidateSelectedItem.ShouldBeNull();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Candidate_Projection_Is_Restored_After_Container_Context_Reset()
|
|
{
|
|
var jack = new SelectOption { Header = "Jack", Content = "jack" };
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
IsMotionEnabled = false,
|
|
OptionsSource = [jack]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var candidateList = GetCandidateList(select);
|
|
var container = GetCandidateContainer(candidateList, 0);
|
|
candidateList.TrySetCandidateItemSelected(jack).ShouldBeTrue();
|
|
container.IsCandidateSelected.ShouldBeTrue();
|
|
|
|
InvokeCandidateListHook(
|
|
candidateList,
|
|
"NotifyClearContainerForVirtualizingContext",
|
|
container);
|
|
container.IsCandidateSelected.ShouldBeFalse();
|
|
|
|
InvokeCandidateListHook(
|
|
candidateList,
|
|
"NotifyRestoreDefaultContext",
|
|
container,
|
|
jack);
|
|
container.IsCandidateSelected.ShouldBeTrue();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Navigation_Skips_Disabled_Hidden_And_Group_Candidates()
|
|
{
|
|
var disabled = new SelectOption
|
|
{
|
|
Header = "Disabled",
|
|
Content = "disabled",
|
|
Group = "A",
|
|
IsEnabled = false
|
|
};
|
|
var selected = new SelectOption
|
|
{
|
|
Header = "Selected",
|
|
Content = "selected",
|
|
Group = "A"
|
|
};
|
|
var available = new SelectOption
|
|
{
|
|
Header = "Available",
|
|
Content = "available",
|
|
Group = "B"
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = SelectMode.Multiple,
|
|
IsGroupEnabled = true,
|
|
GroupPropertySelector = value => (value as ISelectOption)?.Group,
|
|
IsHideSelectedOptions = true,
|
|
OptionsSource = [disabled, selected, available],
|
|
SelectedOptions = [selected]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var candidateList = GetCandidateList(select);
|
|
RaiseCandidateKeyDown(candidateList, Key.Down);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(available);
|
|
|
|
var groupContainer = candidateList.GetVisualDescendants()
|
|
.OfType<SelectCandidateListItem>()
|
|
.First(item => item.IsGroupItem);
|
|
RaisePointerMoved(groupContainer);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(available);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Multiple_Mode_Enter_Toggles_Selection_And_Keeps_Active_Candidate()
|
|
{
|
|
var jack = new SelectOption { Header = "Jack", Content = "jack" };
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = SelectMode.Multiple,
|
|
OptionsSource = [jack]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var candidateList = GetCandidateList(select);
|
|
RaisePointerMoved(GetCandidateContainer(candidateList, 0));
|
|
RaiseCandidateKeyDown(candidateList, Key.Enter);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.SelectedOptions.ShouldNotBeNull();
|
|
select.SelectedOptions.ShouldContain(jack);
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(jack);
|
|
select.IsDropDownOpen.ShouldBeTrue();
|
|
|
|
RaiseCandidateKeyDown(candidateList, Key.Enter);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.SelectedOptions.ShouldNotContain(jack);
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(jack);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Tags_Mode_With_OptionsSource_Creates_Runtime_Option_Without_Mutating_User_Source()
|
|
{
|
|
var optionsSource = new List<ISelectOption>
|
|
{
|
|
new SelectOption { Header = "Jack", Content = "jack" },
|
|
new SelectOption { Header = "Lucy", Content = "lucy" }
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = SelectMode.Tags,
|
|
IsFilterEnabled = true,
|
|
Filter = ValueFilterFactory.BuildFilter(ValueFilterMode.Contains),
|
|
OptionsSource = optionsSource
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var searchTextBox = GetTagsSearchTextBox(select);
|
|
|
|
searchTextBox.Text = "aaa";
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var candidateList = GetCandidateList(select);
|
|
candidateList.TotalItemCount.ShouldBe(1);
|
|
var dynamicOption = candidateList.Items
|
|
.OfType<ISelectOption>()
|
|
.Single(option => option.IsDynamicAdded);
|
|
|
|
dynamicOption.Header.ShouldBe("aaa");
|
|
dynamicOption.Content.ShouldBe("aaa");
|
|
optionsSource.Count.ShouldBe(2);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Tags_Mode_Enter_From_Search_Input_Adds_Runtime_Candidate()
|
|
{
|
|
var optionsSource = new List<ISelectOption>
|
|
{
|
|
new SelectOption { Header = "Jack", Content = "jack" }
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = SelectMode.Tags,
|
|
IsFilterEnabled = true,
|
|
Filter = ValueFilterFactory.BuildFilter(ValueFilterMode.Contains),
|
|
OptionsSource = optionsSource
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var searchTextBox = GetTagsSearchTextBox(select);
|
|
searchTextBox.Text = "ssssss";
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
RaiseSearchInputKeyDown(searchTextBox, Key.Enter);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.SelectedOptions.ShouldNotBeNull();
|
|
select.SelectedOptions.Single().Header.ShouldBe("ssssss");
|
|
select.SelectedOptions.Single().IsDynamicAdded.ShouldBeTrue();
|
|
optionsSource.Count.ShouldBe(1);
|
|
select.IsDropDownOpen.ShouldBeTrue();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Tags_Mode_Escape_From_Search_Input_Closes_Dropdown_Without_Adding_Runtime_Candidate()
|
|
{
|
|
var optionsSource = new List<ISelectOption>
|
|
{
|
|
new SelectOption { Header = "Jack", Content = "jack" }
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = SelectMode.Tags,
|
|
IsFilterEnabled = true,
|
|
Filter = ValueFilterFactory.BuildFilter(ValueFilterMode.Contains),
|
|
OptionsSource = optionsSource
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var searchTextBox = GetTagsSearchTextBox(select);
|
|
searchTextBox.Text = "ssssss";
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
RaiseSearchInputKeyDown(searchTextBox, Key.Escape);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.IsDropDownOpen.ShouldBeFalse();
|
|
select.SelectedOptions.ShouldBeNull();
|
|
optionsSource.Count.ShouldBe(1);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Tags_Mode_Arrow_Keys_From_Search_Input_Navigate_Candidates()
|
|
{
|
|
var jack = new SelectOption { Header = "Jack", Content = "jack" };
|
|
var lucy = new SelectOption { Header = "Lucy", Content = "lucy" };
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = SelectMode.Tags,
|
|
IsFilterEnabled = true,
|
|
Filter = ValueFilterFactory.BuildFilter(ValueFilterMode.Contains),
|
|
OptionsSource = [jack, lucy]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var searchTextBox = GetTagsSearchTextBox(select);
|
|
var candidateList = GetCandidateList(select);
|
|
|
|
RaiseSearchInputKeyDown(searchTextBox, Key.Down);
|
|
Dispatcher.UIThread.RunJobs();
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(jack);
|
|
|
|
RaiseSearchInputKeyDown(searchTextBox, Key.Down);
|
|
Dispatcher.UIThread.RunJobs();
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(lucy);
|
|
|
|
RaiseSearchInputKeyDown(searchTextBox, Key.Up);
|
|
Dispatcher.UIThread.RunJobs();
|
|
candidateList.CandidateSelectedItem.ShouldBeSameAs(jack);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Tags_Mode_Preserves_Selected_Runtime_Option_When_OptionsSource_Replaced_Without_Formal_Match()
|
|
{
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = SelectMode.Tags,
|
|
IsFilterEnabled = true,
|
|
Filter = ValueFilterFactory.BuildFilter(ValueFilterMode.Contains),
|
|
OptionsSource = [new SelectOption { Header = "Jack", Content = "jack" }]
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
var dynamicOption = CreateRuntimeTagOption(select, "aaa");
|
|
|
|
select.SelectedOptions.ShouldNotBeNull();
|
|
select.SelectedOptions.Single().ShouldBeSameAs(dynamicOption);
|
|
|
|
select.OptionsSource = [new SelectOption { Header = "Lucy", Content = "lucy" }];
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.SelectedOptions.ShouldNotBeNull();
|
|
select.SelectedOptions.Single().ShouldBeSameAs(dynamicOption);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Tags_Mode_Remaps_Selected_Runtime_Option_When_OptionsSource_Adds_Formal_Match()
|
|
{
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = SelectMode.Tags,
|
|
IsFilterEnabled = true,
|
|
Filter = ValueFilterFactory.BuildFilter(ValueFilterMode.Contains),
|
|
OptionsSource = [new SelectOption { Header = "Jack", Content = "jack" }]
|
|
};
|
|
var formalOption = new SelectOption
|
|
{
|
|
Header = "AAA formal",
|
|
Content = "aaa"
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
var dynamicOption = CreateRuntimeTagOption(select, "aaa");
|
|
dynamicOption.IsDynamicAdded.ShouldBeTrue();
|
|
|
|
select.OptionsSource = [formalOption];
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.SelectedOptions.ShouldNotBeNull();
|
|
select.SelectedOptions.Single().ShouldBeSameAs(formalOption);
|
|
select.SelectedOptions.Single().IsDynamicAdded.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Tags_Mode_Clearing_Runtime_Option_Does_Not_Mutate_User_Source()
|
|
{
|
|
var optionsSource = new List<ISelectOption>
|
|
{
|
|
new SelectOption { Header = "Jack", Content = "jack" }
|
|
};
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
Mode = SelectMode.Tags,
|
|
IsFilterEnabled = true,
|
|
Filter = ValueFilterFactory.BuildFilter(ValueFilterMode.Contains),
|
|
OptionsSource = optionsSource
|
|
};
|
|
|
|
ShowInWindow(select, () =>
|
|
{
|
|
CreateRuntimeTagOption(select, "aaa");
|
|
|
|
select.ClearValue();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
select.SelectedOptions.ShouldBeNull();
|
|
optionsSource.Count.ShouldBe(1);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Detaching_Select_Cancels_Pending_Async_Options_Load()
|
|
{
|
|
var loader = new PendingSelectOptionsLoader();
|
|
var select = new Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
OptionsLoader = loader
|
|
};
|
|
var window = CreateWindow(select);
|
|
var cancellationObserved = false;
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
InvokeHandleOpenDropRequest(select);
|
|
|
|
loader.Started.Wait(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken).ShouldBeTrue();
|
|
|
|
window.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
cancellationObserved =
|
|
loader.Cancelled.Wait(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
|
|
}
|
|
finally
|
|
{
|
|
loader.Complete();
|
|
window.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
|
|
cancellationObserved.ShouldBeTrue();
|
|
}
|
|
|
|
private static T GetVisualDescendant<T>(Control control, string name)
|
|
where T : Control
|
|
{
|
|
var descendant = control.GetVisualDescendants()
|
|
.OfType<T>()
|
|
.SingleOrDefault(x => x.Name == name);
|
|
descendant.ShouldNotBeNull();
|
|
return descendant;
|
|
}
|
|
|
|
private static T GetPrivateField<T>(object target, string fieldName)
|
|
{
|
|
var field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
|
|
field.ShouldNotBeNull();
|
|
return (T)field!.GetValue(target)!;
|
|
}
|
|
|
|
private static T GetThemeResource<T>(object key)
|
|
{
|
|
var application = Application.Current;
|
|
application.ShouldNotBeNull();
|
|
application!.TryGetResource(key, application.ActualThemeVariant, out var value).ShouldBeTrue();
|
|
value.ShouldBeAssignableTo<T>();
|
|
return (T)value!;
|
|
}
|
|
|
|
private static void ShowInWindow(Control content, Action assertion)
|
|
{
|
|
var window = CreateWindow(content);
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
assertion();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
private static AvaloniaWindow CreateWindow(Control content)
|
|
{
|
|
var overlayPanel = new ScopeAwareOverlayLayerPanel
|
|
{
|
|
Width = 420,
|
|
Height = 320
|
|
};
|
|
overlayPanel.Children.Add(content);
|
|
|
|
var visualLayerManager = new VisualLayerManager
|
|
{
|
|
Child = overlayPanel
|
|
};
|
|
EnablePopupOverlayLayer(visualLayerManager);
|
|
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 420,
|
|
Height = 320,
|
|
Content = visualLayerManager
|
|
};
|
|
|
|
return window;
|
|
}
|
|
|
|
private static SelectCandidateList GetCandidateList(Desktop.Controls.Select select)
|
|
{
|
|
var field = typeof(Desktop.Controls.Select).GetField(
|
|
"_candidateList",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
field.ShouldNotBeNull();
|
|
var candidateList = field.GetValue(select) as SelectCandidateList;
|
|
candidateList.ShouldNotBeNull();
|
|
return candidateList;
|
|
}
|
|
|
|
private static SelectCandidateListItem GetCandidateContainer(SelectCandidateList candidateList, int viewIndex)
|
|
{
|
|
var container = candidateList.ContainerFromIndex(viewIndex) as SelectCandidateListItem;
|
|
container.ShouldNotBeNull();
|
|
return container;
|
|
}
|
|
|
|
private static void RaisePointerMoved(Control source)
|
|
{
|
|
source.RaiseEvent(new PointerEventArgs(
|
|
InputElement.PointerMovedEvent,
|
|
source,
|
|
new Avalonia.Input.Pointer(Avalonia.Input.Pointer.GetNextFreeId(), PointerType.Mouse, true),
|
|
source,
|
|
new Point(1, 1),
|
|
0,
|
|
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.Other),
|
|
KeyModifiers.None));
|
|
}
|
|
|
|
private static void RaiseCandidateKeyDown(SelectCandidateList candidateList, Key key)
|
|
{
|
|
candidateList.HandleKeyDown(new KeyEventArgs
|
|
{
|
|
RoutedEvent = InputElement.KeyDownEvent,
|
|
Source = candidateList,
|
|
Key = key,
|
|
PhysicalKey = key switch
|
|
{
|
|
Key.Enter => PhysicalKey.Enter,
|
|
Key.Up => PhysicalKey.ArrowUp,
|
|
Key.Down => PhysicalKey.ArrowDown,
|
|
_ => PhysicalKey.None
|
|
},
|
|
KeyModifiers = KeyModifiers.None
|
|
});
|
|
}
|
|
|
|
private static void InvokeCandidateListHook(
|
|
SelectCandidateList candidateList,
|
|
string methodName,
|
|
params object?[] arguments)
|
|
{
|
|
var method = typeof(SelectCandidateList).GetMethod(
|
|
methodName,
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
method.ShouldNotBeNull();
|
|
method!.Invoke(candidateList, arguments);
|
|
}
|
|
|
|
private static ISelectOption CreateRuntimeTagOption(Desktop.Controls.Select select, string text)
|
|
{
|
|
select.IsDropDownOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var searchTextBox = GetTagsSearchTextBox(select);
|
|
searchTextBox.Text = text;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var candidateList = GetCandidateList(select);
|
|
var dynamicOption = candidateList.Items
|
|
.OfType<ISelectOption>()
|
|
.Single(option => option.IsDynamicAdded);
|
|
candidateList.CandidateSelectedItem = dynamicOption;
|
|
candidateList.HandleKeyDown(new KeyEventArgs
|
|
{
|
|
RoutedEvent = InputElement.KeyDownEvent,
|
|
Source = candidateList,
|
|
Key = Key.Enter,
|
|
PhysicalKey = PhysicalKey.Enter,
|
|
KeyModifiers = KeyModifiers.None
|
|
});
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
return dynamicOption;
|
|
}
|
|
|
|
private static void AssertEmbeddedSelectFilterTextBox(SelectFilterTextBox searchTextBox)
|
|
{
|
|
searchTextBox.ShouldBeAssignableTo<EmbeddedTextBox>();
|
|
searchTextBox.SizeType.ShouldBe(CustomizableSizeType.Custom);
|
|
searchTextBox.Padding.ShouldBe(new Thickness(0));
|
|
searchTextBox.BorderThickness.ShouldBe(new Thickness(0));
|
|
searchTextBox.IsCustomFontSize.ShouldBeTrue();
|
|
}
|
|
|
|
private static SelectFilterTextBox GetTagsSearchTextBox(Desktop.Controls.Select select)
|
|
{
|
|
var textBox = select.GetVisualDescendants()
|
|
.OfType<SelectFilterTextBox>()
|
|
.SingleOrDefault(item => item.Name != "PART_SingleFilterInput");
|
|
textBox.ShouldNotBeNull();
|
|
return textBox;
|
|
}
|
|
|
|
private static void RaiseSearchInputKeyDown(SelectFilterTextBox searchTextBox, Key key)
|
|
{
|
|
searchTextBox.RaiseEvent(new KeyEventArgs
|
|
{
|
|
RoutedEvent = InputElement.KeyDownEvent,
|
|
Source = searchTextBox,
|
|
Key = key,
|
|
PhysicalKey = key switch
|
|
{
|
|
Key.Enter => PhysicalKey.Enter,
|
|
Key.Escape => PhysicalKey.Escape,
|
|
Key.Up => PhysicalKey.ArrowUp,
|
|
Key.Down => PhysicalKey.ArrowDown,
|
|
_ => PhysicalKey.None
|
|
},
|
|
KeyModifiers = KeyModifiers.None
|
|
});
|
|
}
|
|
|
|
private static void InvokeHandleOpenDropRequest(Desktop.Controls.Select select)
|
|
{
|
|
var method = typeof(Desktop.Controls.Select).GetMethod(
|
|
"HandleOpenDropRequest",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
method.ShouldNotBeNull();
|
|
method.Invoke(select, null);
|
|
}
|
|
|
|
private static void EnablePopupOverlayLayer(VisualLayerManager visualLayerManager)
|
|
{
|
|
var property = typeof(VisualLayerManager).GetProperty(
|
|
"EnablePopupOverlayLayer",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
property.ShouldNotBeNull();
|
|
property.SetValue(visualLayerManager, true);
|
|
}
|
|
|
|
private sealed class PendingSelectOptionsLoader : ISelectOptionsAsyncLoader
|
|
{
|
|
private readonly TaskCompletionSource<SelectOptionsLoadResult> _completion =
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
public ManualResetEventSlim Started { get; } = new();
|
|
public ManualResetEventSlim Cancelled { get; } = new();
|
|
|
|
public Task<SelectOptionsLoadResult> LoadAsync(object? context, CancellationToken token)
|
|
{
|
|
Started.Set();
|
|
token.Register(static state => ((ManualResetEventSlim)state!).Set(), Cancelled);
|
|
return _completion.Task;
|
|
}
|
|
|
|
public void Complete()
|
|
{
|
|
_completion.TrySetResult(new SelectOptionsLoadResult
|
|
{
|
|
Data = []
|
|
});
|
|
}
|
|
}
|
|
|
|
private sealed class SelectBindingViewModel : INotifyPropertyChanged
|
|
{
|
|
private ISelectOption? _selectedOption;
|
|
private IList<ISelectOption>? _selectedOptions;
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
public ISelectOption? SelectedOption
|
|
{
|
|
get => _selectedOption;
|
|
set
|
|
{
|
|
if (ReferenceEquals(_selectedOption, value))
|
|
{
|
|
return;
|
|
}
|
|
_selectedOption = value;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedOption)));
|
|
}
|
|
}
|
|
|
|
public IList<ISelectOption>? SelectedOptions
|
|
{
|
|
get => _selectedOptions;
|
|
set
|
|
{
|
|
if (ReferenceEquals(_selectedOptions, value))
|
|
{
|
|
return;
|
|
}
|
|
_selectedOptions = value;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedOptions)));
|
|
}
|
|
}
|
|
}
|
|
}
|