mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-26 03:00:28 +08:00
561 lines
20 KiB
C#
561 lines
20 KiB
C#
using AtomUI.Controls.Data;
|
|
using AtomUI.Desktop.Controls.Primitives;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Headless;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.Dialog;
|
|
|
|
public class DialogPopupControlFamilyTests
|
|
{
|
|
static DialogPopupControlFamilyTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
public static TheoryData<string> OpenStateControlFamilies =>
|
|
new()
|
|
{
|
|
"ComboBox",
|
|
"Select",
|
|
"Cascader",
|
|
"TreeSelect",
|
|
"AutoComplete",
|
|
"AutoCompleteSearchEdit",
|
|
"AutoCompleteTextArea",
|
|
"Mentions"
|
|
};
|
|
|
|
public static TheoryData<string> PickerControlFamilies =>
|
|
new()
|
|
{
|
|
"DatePicker",
|
|
"TimePicker",
|
|
"RangeDatePicker",
|
|
"RangeTimePicker"
|
|
};
|
|
|
|
public static TheoryData<string> ColorPickerControlFamilies =>
|
|
new()
|
|
{
|
|
"ColorPicker",
|
|
"GradientColorPicker"
|
|
};
|
|
|
|
[Theory]
|
|
[MemberData(nameof(OpenStateControlFamilies))]
|
|
public void Open_State_Control_Family_In_Dialog_Creates_A_Real_Popup_Host(string family)
|
|
{
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var scenario = CreateOpenStateScenario(family);
|
|
using var host = DialogPopupTestHost.Open(scenario.Control);
|
|
|
|
scenario.Open(host);
|
|
DialogPopupTestHost.Pump();
|
|
|
|
host.FindPopupHost().ShouldNotBeNull();
|
|
scenario.IsOpen().ShouldBeTrue();
|
|
|
|
host.LightDismiss();
|
|
scenario.IsOpen().ShouldBeFalse();
|
|
host.AssertNoPopupHosts();
|
|
host.Presenter.Parent.ShouldBeSameAs(host.DialogLayer);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(PickerControlFamilies))]
|
|
public void Picker_Family_In_Dialog_Uses_The_Shared_InfoPicker_Popup_Path(string family)
|
|
{
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var picker = CreatePicker(family);
|
|
using var host = DialogPopupTestHost.Open(picker);
|
|
var input = picker.GetVisualDescendants()
|
|
.OfType<TextBox>()
|
|
.First(control => control.Name == "PART_InfoInputBox");
|
|
|
|
host.Click(input);
|
|
|
|
host.FindPopupHost().ShouldNotBeNull();
|
|
|
|
host.LightDismiss();
|
|
host.AssertNoPopupHosts();
|
|
host.Presenter.Parent.ShouldBeSameAs(host.DialogLayer);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ColorPickerControlFamilies))]
|
|
public void ColorPicker_Family_In_Dialog_Uses_Its_Independent_Popup_Template(string family)
|
|
{
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
Control picker = family switch
|
|
{
|
|
"ColorPicker" => new AtomUI.Desktop.Controls.ColorPicker { Width = 220, IsMotionEnabled = false },
|
|
"GradientColorPicker" => new AtomUI.Desktop.Controls.GradientColorPicker { Width = 220, IsMotionEnabled = false },
|
|
_ => throw new ArgumentOutOfRangeException(nameof(family), family, null)
|
|
};
|
|
using var host = DialogPopupTestHost.Open(picker);
|
|
|
|
host.Click(picker);
|
|
|
|
host.FindPopupHost().ShouldNotBeNull();
|
|
host.LightDismiss();
|
|
host.AssertNoPopupHosts();
|
|
host.Presenter.Parent.ShouldBeSameAs(host.DialogLayer);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Menu_Submenu_In_Dialog_Uses_The_Owning_Window_Popup_Layer()
|
|
{
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var child = new AtomUI.Desktop.Controls.MenuItem { Header = "Child" };
|
|
var parent = new AtomUI.Desktop.Controls.MenuItem
|
|
{
|
|
Header = "Parent",
|
|
IsMotionEnabled = false,
|
|
ShouldUseOverlayPopup = true,
|
|
Items = { child }
|
|
};
|
|
var menu = new AtomUI.Desktop.Controls.Menu { Items = { parent } };
|
|
using var host = DialogPopupTestHost.Open(menu);
|
|
|
|
parent.IsSubMenuOpen = true;
|
|
|
|
host.FindPopupHost().ShouldNotBeNull();
|
|
parent.IsSubMenuOpen.ShouldBeTrue();
|
|
host.ClickOutsidePopup();
|
|
parent.IsSubMenuOpen.ShouldBeFalse();
|
|
host.AssertNoPopupHosts();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void NavMenu_Submenu_In_Dialog_Uses_The_Owning_Window_Popup_Layer()
|
|
{
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var parentNode = new AtomUI.Desktop.Controls.NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
Children =
|
|
{
|
|
new AtomUI.Desktop.Controls.NavMenuNode { Header = "Child" }
|
|
}
|
|
};
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Width = 240,
|
|
Mode = AtomUI.Desktop.Controls.NavMenuMode.Vertical,
|
|
IsMotionEnabled = false,
|
|
ShouldUseOverlayPopup = true,
|
|
Items = { parentNode }
|
|
};
|
|
using var host = DialogPopupTestHost.Open(menu);
|
|
var parent = menu.ContainerFromItem(parentNode)
|
|
.ShouldBeOfType<AtomUI.Desktop.Controls.NavMenuItem>();
|
|
|
|
parent.IsSubMenuOpen = true;
|
|
|
|
host.FindPopupHost().ShouldNotBeNull();
|
|
parent.IsSubMenuOpen.ShouldBeTrue();
|
|
host.ClickOutsidePopup();
|
|
parent.IsSubMenuOpen.ShouldBeFalse();
|
|
host.AssertNoPopupHosts();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Tour_In_Dialog_Uses_Its_Target_In_The_Owning_Window()
|
|
{
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var target = new AtomUI.Desktop.Controls.Button
|
|
{
|
|
Width = 100,
|
|
Height = 32,
|
|
Content = "Tour target"
|
|
};
|
|
var tour = new AtomUI.Desktop.Controls.Tour
|
|
{
|
|
IsMotionEnabled = false,
|
|
IsShowMask = false
|
|
};
|
|
tour.Steps.Add(new AtomUI.Desktop.Controls.TourStep
|
|
{
|
|
Target = target,
|
|
Title = "Tour step",
|
|
Content = "Tour content"
|
|
});
|
|
var content = new Avalonia.Controls.Grid { Children = { target, tour } };
|
|
using var host = DialogPopupTestHost.Open(content);
|
|
|
|
tour.IsOpen = true;
|
|
|
|
host.FindPopupHost().ShouldNotBeNull();
|
|
tour.IsOpen.ShouldBeTrue();
|
|
TopLevel.GetTopLevel(target).ShouldBeSameAs(host.Window);
|
|
|
|
tour.IsOpen = false;
|
|
DialogPopupTestHost.Pump();
|
|
tour.IsOpen.ShouldBeFalse();
|
|
host.AssertNoPopupHosts();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DropdownButton_In_Dialog_Opens_And_Invokes_Its_MenuFlyout()
|
|
{
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var invoked = false;
|
|
var menuItem = new AtomUI.Desktop.Controls.MenuItem { Header = "Action" };
|
|
menuItem.Click += (_, _) => invoked = true;
|
|
var flyout = new AtomUI.Desktop.Controls.MenuFlyout
|
|
{
|
|
IsMotionEnabled = false,
|
|
ShouldUseOverlayPopup = true,
|
|
Items = { menuItem }
|
|
};
|
|
var button = new AtomUI.Desktop.Controls.DropdownButton
|
|
{
|
|
Content = "Open",
|
|
TriggerType = AtomUI.Desktop.Controls.FlyoutTriggerType.Click,
|
|
DropdownFlyout = flyout,
|
|
IsMotionEnabled = false
|
|
};
|
|
using var host = DialogPopupTestHost.Open(button);
|
|
|
|
host.Click(button);
|
|
var popupHost = host.FindPopupHost();
|
|
TopLevel.GetTopLevel(popupHost).ShouldBeSameAs(host.Window);
|
|
var popupItem = popupHost.GetVisualDescendants()
|
|
.OfType<AtomUI.Desktop.Controls.MenuItem>()
|
|
.Single(item => item.Header?.ToString() == "Action");
|
|
|
|
host.Click(popupItem);
|
|
invoked.ShouldBeTrue();
|
|
flyout.IsOpen.ShouldBeFalse();
|
|
host.AssertNoPopupHosts();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void SplitButton_In_Dialog_Opens_Its_Flyout_From_The_Secondary_Button()
|
|
{
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var flyout = new AtomUI.Desktop.Controls.Flyout
|
|
{
|
|
Content = new TextBlock { Text = "Split content" },
|
|
IsMotionEnabled = false,
|
|
ShouldUseOverlayPopup = true
|
|
};
|
|
var splitButton = new AtomUI.Desktop.Controls.SplitButton
|
|
{
|
|
Content = "Split",
|
|
TriggerType = AtomUI.Desktop.Controls.FlyoutTriggerType.Click,
|
|
Flyout = flyout,
|
|
IsMotionEnabled = false
|
|
};
|
|
using var host = DialogPopupTestHost.Open(splitButton);
|
|
var secondaryButton = splitButton.GetVisualDescendants()
|
|
.OfType<AtomUI.Desktop.Controls.Button>()
|
|
.Single(button => button.Name == "PART_SecondaryButton");
|
|
|
|
host.Click(secondaryButton);
|
|
TopLevel.GetTopLevel(host.FindPopupHost()).ShouldBeSameAs(host.Window);
|
|
flyout.IsOpen.ShouldBeTrue();
|
|
|
|
host.LightDismiss();
|
|
flyout.IsOpen.ShouldBeFalse();
|
|
host.AssertNoPopupHosts();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void PopupConfirm_In_Dialog_Confirms_And_Closes_Its_Flyout()
|
|
{
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var confirmed = false;
|
|
var popupConfirm = new AtomUI.Desktop.Controls.PopupConfirm
|
|
{
|
|
Content = new AtomUI.Desktop.Controls.Button { Content = "Delete" },
|
|
Title = "Confirm",
|
|
OkText = "Confirm",
|
|
CancelText = "Cancel",
|
|
IsMotionEnabled = false,
|
|
ShouldUseOverlayPopup = true
|
|
};
|
|
popupConfirm.Confirmed += (_, _) => confirmed = true;
|
|
using var host = DialogPopupTestHost.Open(popupConfirm);
|
|
|
|
popupConfirm.ShowFlyout(true);
|
|
var popupHost = host.FindPopupHost();
|
|
var okButton = popupHost.GetVisualDescendants()
|
|
.OfType<AtomUI.Desktop.Controls.Button>()
|
|
.Single(button => button.Name == "PART_OkButton");
|
|
|
|
host.Click(okButton);
|
|
confirmed.ShouldBeTrue();
|
|
popupConfirm.Flyout!.IsOpen.ShouldBeFalse();
|
|
host.AssertNoPopupHosts();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void AvatarGroup_Fold_In_Dialog_Opens_Its_Flyout()
|
|
{
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var avatarGroup = new AtomUI.Desktop.Controls.AvatarGroup
|
|
{
|
|
MaxDisplayCount = 2,
|
|
FoldAvatarFlyoutTriggerType = AtomUI.Desktop.Controls.FlyoutTriggerType.Click,
|
|
IsMotionEnabled = false,
|
|
Children =
|
|
{
|
|
new AtomUI.Controls.Avatar { Text = "A" },
|
|
new AtomUI.Controls.Avatar { Text = "B" },
|
|
new AtomUI.Controls.Avatar { Text = "C" },
|
|
new AtomUI.Controls.Avatar { Text = "D" }
|
|
}
|
|
};
|
|
using var host = DialogPopupTestHost.Open(avatarGroup);
|
|
var foldHost = avatarGroup.GetVisualDescendants()
|
|
.OfType<AtomUI.Desktop.Controls.FlyoutHost>()
|
|
.Single();
|
|
var foldAvatar = foldHost.Content.ShouldBeOfType<AtomUI.Controls.Avatar>();
|
|
|
|
host.Click(foldAvatar);
|
|
TopLevel.GetTopLevel(host.FindPopupHost()).ShouldBeSameAs(host.Window);
|
|
foldHost.Flyout!.IsOpen.ShouldBeTrue();
|
|
|
|
var foldFlyout = foldHost.Flyout;
|
|
avatarGroup.MaxDisplayCount = null;
|
|
host.WaitForClosed(() => foldFlyout.IsOpen);
|
|
host.AssertNoPopupHosts();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Transfer_Dropdown_In_Dialog_Opens_Its_MenuFlyout()
|
|
{
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var transfer = new AtomUI.Desktop.Controls.ListTransfer
|
|
{
|
|
Width = 360,
|
|
Height = 220,
|
|
PageSize = 2,
|
|
IsMotionEnabled = false,
|
|
ItemsSource = Enumerable.Range(1, 4)
|
|
.Select(index => new ListItemData
|
|
{
|
|
ItemKey = $"item-{index}",
|
|
Content = $"Item {index}"
|
|
})
|
|
.ToList()
|
|
};
|
|
using var host = DialogPopupTestHost.Open(transfer);
|
|
var dropdown = transfer.GetVisualDescendants()
|
|
.OfType<AtomUI.Desktop.Controls.TransferSelectDropdown>()
|
|
.First();
|
|
|
|
host.Invoke(dropdown);
|
|
var menuFlyout = dropdown.Flyout.ShouldBeOfType<AtomUI.Desktop.Controls.MenuFlyout>();
|
|
menuFlyout.IsOpen.ShouldBeTrue();
|
|
TopLevel.GetTopLevel(host.FindPopupHost()).ShouldBeSameAs(host.Window);
|
|
|
|
host.LightDismiss();
|
|
menuFlyout.IsOpen.ShouldBeFalse();
|
|
host.AssertNoPopupHosts();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void TabControl_Overflow_In_Dialog_Opens_Its_MenuFlyout()
|
|
{
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
var tabControl = new AtomUI.Desktop.Controls.TabControl
|
|
{
|
|
Width = 180,
|
|
Height = 180,
|
|
SelectedIndex = 0,
|
|
IsMotionEnabled = false
|
|
};
|
|
foreach (var index in Enumerable.Range(1, 10))
|
|
{
|
|
tabControl.Items.Add(new AtomUI.Desktop.Controls.TabItem
|
|
{
|
|
Header = $"Long tab {index}",
|
|
Content = $"Content {index}"
|
|
});
|
|
}
|
|
|
|
using var host = DialogPopupTestHost.Open(tabControl);
|
|
AvaloniaHeadlessPlatform.ForceRenderTimerTick(1);
|
|
DialogPopupTestHost.Pump();
|
|
var scrollViewer = tabControl.GetVisualDescendants()
|
|
.OfType<AtomUI.Desktop.Controls.TabControlScrollViewer>()
|
|
.Single();
|
|
scrollViewer.Extent.Width.ShouldBeGreaterThan(scrollViewer.Viewport.Width);
|
|
var menuIndicator = scrollViewer.GetVisualDescendants()
|
|
.OfType<AtomUI.Desktop.Controls.IconButton>()
|
|
.Single(button => button.Name == "PART_ScrollMenuIndicator");
|
|
|
|
host.Click(menuIndicator);
|
|
var popupHost = host.FindPopupHost();
|
|
TopLevel.GetTopLevel(popupHost).ShouldBeSameAs(host.Window);
|
|
|
|
host.LightDismiss();
|
|
host.AssertNoPopupHosts();
|
|
});
|
|
}
|
|
|
|
private static OpenStateScenario CreateOpenStateScenario(string family)
|
|
{
|
|
switch (family)
|
|
{
|
|
case "ComboBox":
|
|
{
|
|
var control = new AtomUI.Desktop.Controls.ComboBox
|
|
{
|
|
Width = 240,
|
|
IsMotionEnabled = false,
|
|
Items =
|
|
{
|
|
new AtomUI.Desktop.Controls.ComboBoxItem { Content = "One" },
|
|
new AtomUI.Desktop.Controls.ComboBoxItem { Content = "Two" }
|
|
}
|
|
};
|
|
return new OpenStateScenario(control, _ => control.IsDropDownOpen = true, () => control.IsDropDownOpen);
|
|
}
|
|
case "Select":
|
|
{
|
|
var control = new AtomUI.Desktop.Controls.Select
|
|
{
|
|
Width = 240,
|
|
IsMotionEnabled = false,
|
|
OptionsSource =
|
|
[
|
|
new AtomUI.Desktop.Controls.SelectOption { Header = "One" },
|
|
new AtomUI.Desktop.Controls.SelectOption { Header = "Two" }
|
|
]
|
|
};
|
|
return new OpenStateScenario(control, _ => control.IsDropDownOpen = true, () => control.IsDropDownOpen);
|
|
}
|
|
case "Cascader":
|
|
{
|
|
var control = new AtomUI.Desktop.Controls.Cascader
|
|
{
|
|
Width = 240,
|
|
IsMotionEnabled = false,
|
|
OptionsSource = new AtomUI.Desktop.Controls.ICascaderOption[]
|
|
{
|
|
new AtomUI.Desktop.Controls.CascaderOption { Header = "One", Value = "one", IsLeaf = true },
|
|
new AtomUI.Desktop.Controls.CascaderOption { Header = "Two", Value = "two", IsLeaf = true }
|
|
}
|
|
};
|
|
return new OpenStateScenario(control, _ => control.IsDropDownOpen = true, () => control.IsDropDownOpen);
|
|
}
|
|
case "TreeSelect":
|
|
{
|
|
var control = new AtomUI.Desktop.Controls.TreeSelect
|
|
{
|
|
Width = 240,
|
|
IsMotionEnabled = false,
|
|
ItemsSource =
|
|
[
|
|
new AtomUI.Desktop.Controls.TreeItemNode { Header = "One", Value = "one", IsLeaf = true },
|
|
new AtomUI.Desktop.Controls.TreeItemNode { Header = "Two", Value = "two", IsLeaf = true }
|
|
]
|
|
};
|
|
return new OpenStateScenario(control, _ => control.IsDropDownOpen = true, () => control.IsDropDownOpen);
|
|
}
|
|
case "AutoComplete":
|
|
{
|
|
var control = new AtomUI.Desktop.Controls.AutoComplete();
|
|
return CreateAutoCompleteScenario(control);
|
|
}
|
|
case "AutoCompleteSearchEdit":
|
|
{
|
|
var control = new AtomUI.Desktop.Controls.AutoCompleteSearchEdit();
|
|
return CreateAutoCompleteScenario(control);
|
|
}
|
|
case "AutoCompleteTextArea":
|
|
{
|
|
var control = new AtomUI.Desktop.Controls.AutoCompleteTextArea();
|
|
return CreateAutoCompleteScenario(control);
|
|
}
|
|
case "Mentions":
|
|
{
|
|
var control = new AtomUI.Desktop.Controls.Mentions
|
|
{
|
|
Width = 240,
|
|
IsMotionEnabled = false,
|
|
OptionsSource =
|
|
[
|
|
new AtomUI.Desktop.Controls.MentionOption { Header = "One", Value = "one" },
|
|
new AtomUI.Desktop.Controls.MentionOption { Header = "Two", Value = "two" }
|
|
]
|
|
};
|
|
return new OpenStateScenario(control, _ => control.IsDropDownOpen = true, () => control.IsDropDownOpen);
|
|
}
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(family), family, null);
|
|
}
|
|
}
|
|
|
|
private static OpenStateScenario CreateAutoCompleteScenario(
|
|
AtomUI.Desktop.Controls.AbstractAutoComplete control)
|
|
{
|
|
control.Width = 240;
|
|
control.IsMotionEnabled = false;
|
|
control.MinimumPrefixLength = 0;
|
|
control.OptionsSource =
|
|
[
|
|
new AtomUI.Desktop.Controls.AutoCompleteOption { Header = "One", Content = "One" },
|
|
new AtomUI.Desktop.Controls.AutoCompleteOption { Header = "Two", Content = "Two" }
|
|
];
|
|
return new OpenStateScenario(
|
|
control,
|
|
_ => control.IsDropDownOpen = true,
|
|
() => control.IsDropDownOpen);
|
|
}
|
|
|
|
private static InfoPickerInput CreatePicker(string family)
|
|
{
|
|
InfoPickerInput picker = family switch
|
|
{
|
|
"DatePicker" => new AtomUI.Desktop.Controls.DatePicker(),
|
|
"TimePicker" => new AtomUI.Desktop.Controls.TimePicker(),
|
|
"RangeDatePicker" => new AtomUI.Desktop.Controls.RangeDatePicker(),
|
|
"RangeTimePicker" => new AtomUI.Desktop.Controls.RangeTimePicker(),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(family), family, null)
|
|
};
|
|
picker.Width = 240;
|
|
picker.PlaceholderText = "Select a value";
|
|
picker.IsMotionEnabled = false;
|
|
return picker;
|
|
}
|
|
|
|
private sealed record OpenStateScenario(
|
|
Control Control,
|
|
Action<DialogPopupTestHost> Open,
|
|
Func<bool> IsOpen);
|
|
}
|