Files

1223 lines
37 KiB
C#

using System.Reflection;
using AtomUI.Controls.Primitives;
using AtomUI.MotionScene;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Threading;
using Shouldly;
using Xunit;
namespace AtomUI.Desktop.Controls.Tests.NavMenu;
public class NavMenuSelectionTests
{
static NavMenuSelectionTests()
{
AvaloniaTestApp.EnsureInitialized();
}
[Fact]
public void NavMenu_Ignores_Stale_Async_SelectedItem_Replay()
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = false
};
var first = new NavMenuNode
{
Header = "First",
ItemKey = "first"
};
var second = new NavMenuNode
{
Header = "Second",
ItemKey = "second"
};
menu.Items.Add(first);
menu.Items.Add(second);
var selectedNodes = new List<INavMenuNode>();
menu.NavMenuNodeSelected += (_, args) => selectedNodes.Add(args.NavMenuNode);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
menu.SelectedItem = first;
menu.SelectedItem = second;
Dispatcher.UIThread.RunJobs();
selectedNodes.ShouldBe([second]);
menu.SelectedItem.ShouldBeSameAs(second);
}
finally
{
window.Close();
}
}
[Theory]
[InlineData(NavMenuMode.Horizontal)]
[InlineData(NavMenuMode.Vertical)]
[InlineData(NavMenuMode.Inline)]
public void DefaultSelectedPath_Selects_Nested_Item_Without_Waiting_For_Timer(NavMenuMode mode)
{
var leaf = new NavMenuNode
{
Header = "Leaf",
ItemKey = "leaf"
};
var child = new NavMenuNode
{
Header = "Child",
ItemKey = "child"
};
child.Children.Add(leaf);
var parent = new NavMenuNode
{
Header = "Parent",
ItemKey = "parent"
};
parent.Children.Add(child);
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = mode,
IsMotionEnabled = false,
DefaultSelectedPath = new TreeNodePath("parent/child/leaf")
};
menu.Items.Add(parent);
var selectedNodes = new List<INavMenuNode>();
menu.NavMenuNodeSelected += (_, args) => selectedNodes.Add(args.NavMenuNode);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = mode == NavMenuMode.Inline ? menu : CreatePopupOverlayHost(menu)
};
try
{
window.Show();
RunDispatcherJobsUntil(() => ReferenceEquals(menu.SelectedItem, leaf));
menu.SelectedItem.ShouldBeSameAs(leaf);
selectedNodes.ShouldBe([leaf]);
}
finally
{
window.Close();
}
}
[Fact]
public void DefaultSelectedPath_Traverses_Groups_Without_Consuming_Path_Segments()
{
var leaf = new NavMenuNode
{
Header = "Leaf",
ItemKey = "leaf"
};
var childGroup = new NavMenuGroup { Header = "Child group" };
childGroup.Entries.Add(leaf);
var parent = new NavMenuNode
{
Header = "Parent",
ItemKey = "parent"
};
parent.Entries.Add(childGroup);
var rootGroup = new NavMenuGroup { Header = "Root group" };
rootGroup.Entries.Add(parent);
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = false,
DefaultSelectedPath = new TreeNodePath("parent/leaf")
};
menu.Items.Add(rootGroup);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
RunDispatcherJobsUntil(() => ReferenceEquals(menu.SelectedItem, leaf));
var rootGroupContainer = menu.ContainerFromItem(rootGroup).ShouldBeOfType<NavMenuGroupItem>();
var parentContainer = rootGroupContainer.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
var childGroupContainer = parentContainer.ContainerFromItem(childGroup).ShouldBeOfType<NavMenuGroupItem>();
var leafContainer = childGroupContainer.ContainerFromItem(leaf).ShouldBeOfType<NavMenuItem>();
menu.SelectedItem.ShouldBeSameAs(leaf);
parentContainer.IsInSelectedPath.ShouldBeTrue();
leafContainer.IsSelected.ShouldBeTrue();
}
finally
{
window.Close();
}
}
[Theory]
[InlineData(NavMenuMode.Horizontal)]
[InlineData(NavMenuMode.Vertical)]
[InlineData(NavMenuMode.Inline)]
public void DefaultOpenPaths_Opens_Nested_Submenu_Without_Waiting_For_Timer(NavMenuMode mode)
{
var leaf = new NavMenuNode
{
Header = "Leaf",
ItemKey = "leaf"
};
var child = new NavMenuNode
{
Header = "Child",
ItemKey = "child"
};
child.Children.Add(leaf);
var parent = new NavMenuNode
{
Header = "Parent",
ItemKey = "parent"
};
parent.Children.Add(child);
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = mode,
IsMotionEnabled = false,
DefaultOpenPaths =
[
new TreeNodePath("parent/child")
]
};
menu.Items.Add(parent);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = mode == NavMenuMode.Inline ? menu : CreatePopupOverlayHost(menu)
};
try
{
window.Show();
RunDispatcherJobsUntil(() =>
{
var parentContainer = menu.ContainerFromItem(parent) as NavMenuItem;
var childContainer = parentContainer?.ContainerFromItem(child) as NavMenuItem;
return parentContainer?.IsSubMenuOpen == true &&
childContainer?.IsSubMenuOpen == true;
});
var realizedParent = menu.ContainerFromItem(parent) as NavMenuItem;
var realizedChild = realizedParent?.ContainerFromItem(child) as NavMenuItem;
realizedParent.ShouldNotBeNull();
realizedChild.ShouldNotBeNull();
realizedParent.IsSubMenuOpen.ShouldBeTrue();
realizedChild.IsSubMenuOpen.ShouldBeTrue();
}
finally
{
window.Close();
}
}
[Theory]
[InlineData(NavMenuMode.Vertical)]
[InlineData(NavMenuMode.Horizontal)]
[InlineData(NavMenuMode.Inline)]
public void NavMenu_Clears_Selected_Container_When_SelectedItem_Is_Set_To_Null(NavMenuMode mode)
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = mode,
IsMotionEnabled = false
};
var first = new NavMenuNode
{
Header = "First",
ItemKey = "first"
};
menu.Items.Add(first);
var selectedNodes = new List<INavMenuNode>();
menu.NavMenuNodeSelected += (_, args) => selectedNodes.Add(args.NavMenuNode);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
menu.SelectedItem = first;
Dispatcher.UIThread.RunJobs();
var firstContainer = menu.ContainerFromItem(first) as Control;
firstContainer.ShouldNotBeNull();
IsSelected(firstContainer).ShouldBeTrue();
menu.SelectedItem = null;
Dispatcher.UIThread.RunJobs();
menu.SelectedItem.ShouldBeNull();
IsSelected(firstContainer).ShouldBeFalse();
selectedNodes.Clear();
menu.SelectedItem = first;
Dispatcher.UIThread.RunJobs();
IsSelected(firstContainer).ShouldBeTrue();
selectedNodes.ShouldBe([first]);
}
finally
{
window.Close();
}
}
[Fact]
public void NavMenu_Selects_New_Item_After_Replacing_The_Selected_Item_Tree()
{
var first = new NavMenuNode
{
Header = "First",
ItemKey = "first"
};
var replacement = new NavMenuNode
{
Header = "Replacement",
ItemKey = "replacement"
};
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = false
};
menu.Items.Add(first);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var firstContainer = menu.ContainerFromItem(first).ShouldBeOfType<NavMenuItem>();
menu.InteractionHandler.ShouldNotBeNull();
menu.SelectNavMenuItem(firstContainer);
menu.Items.Clear();
menu.Items.Add(replacement);
Dispatcher.UIThread.RunJobs();
var replacementContainer = menu.ContainerFromItem(replacement).ShouldBeOfType<NavMenuItem>();
var replacementHeader = GetItemHeader(replacementContainer);
var replacementPoint = GetCenterPoint(replacementHeader, window);
Should.NotThrow(() =>
{
window.MouseDown(replacementPoint, MouseButton.Left);
Dispatcher.UIThread.RunJobs();
window.MouseUp(replacementPoint, MouseButton.Left);
Dispatcher.UIThread.RunJobs();
});
replacementContainer.IsSelected.ShouldBeTrue();
menu.SelectedItem.ShouldBeSameAs(replacement);
}
finally
{
window.Close();
}
}
[Fact]
public void NavMenu_Detach_Then_Programmatic_Sibling_Selection_Clears_Previous_Selection()
{
var first = new NavMenuNode
{
Header = "First",
ItemKey = "first"
};
var second = new NavMenuNode
{
Header = "Second",
ItemKey = "second"
};
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = false
};
menu.Items.Add(first);
menu.Items.Add(second);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
menu.SelectedItem = first;
Dispatcher.UIThread.RunJobs();
var firstContainer = menu.ContainerFromItem(first).ShouldBeOfType<NavMenuItem>();
firstContainer.IsSelected.ShouldBeTrue();
window.Content = null;
Dispatcher.UIThread.RunJobs();
menu.SelectedItem = second;
window.Content = menu;
Dispatcher.UIThread.RunJobs();
var secondContainer = menu.ContainerFromItem(second).ShouldBeOfType<NavMenuItem>();
firstContainer.IsSelected.ShouldBeFalse(
"visual detach must not discard the identity needed to clear the applied selection.");
secondContainer.IsSelected.ShouldBeTrue();
}
finally
{
window.Close();
}
}
[Fact]
public void Inline_Submenu_Child_Press_Does_Not_Apply_Active_Background_To_Parent_Header()
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = false
};
var child = new NavMenuNode
{
Header = "Child",
ItemKey = "child"
};
var parent = new NavMenuNode
{
Header = "Parent",
ItemKey = "parent"
};
parent.Children.Add(child);
menu.Items.Add(parent);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
var parentMenuItem = (INavMenuItem)parentContainer;
parentMenuItem.Open();
Dispatcher.UIThread.RunJobs();
var childContainer = (Control)((ItemsControl)parentContainer).ContainerFromItem(child)!;
var parentHeader = GetItemHeader(parentContainer);
var childHeader = GetItemHeader(childContainer);
MouseDown(childHeader, window);
Dispatcher.UIThread.RunJobs();
GetSolidBrushColor(parentHeader.Background).ShouldBe(
Colors.Transparent,
"pressing a child menu item must not make its parent header look active");
}
finally
{
window.Close();
}
}
[Fact]
public void Inline_Selection_Marks_All_Ancestor_Submenus_As_Selected_Path()
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = false
};
var leaf = new NavMenuNode
{
Header = "Option 1",
ItemKey = "option-1"
};
var secondLevel = new NavMenuNode
{
Header = "Item 1",
ItemKey = "item-1"
};
secondLevel.Children.Add(leaf);
var firstLevel = new NavMenuNode
{
Header = "Navigation Three - Submenu",
ItemKey = "navigation-three"
};
firstLevel.Children.Add(secondLevel);
menu.Items.Add(firstLevel);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 360,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var firstLevelContainer = (NavMenuItem)menu.ContainerFromItem(firstLevel)!;
firstLevelContainer.Open();
Dispatcher.UIThread.RunJobs();
var secondLevelContainer = (NavMenuItem)firstLevelContainer.ContainerFromItem(secondLevel)!;
secondLevelContainer.Open();
Dispatcher.UIThread.RunJobs();
var leafContainer = (NavMenuItem)secondLevelContainer.ContainerFromItem(leaf)!;
menu.InteractionHandler.ShouldNotBeNull();
menu.SelectNavMenuItem(leafContainer);
Dispatcher.UIThread.RunJobs();
firstLevelContainer.IsInSelectedPath.ShouldBeTrue(
"Ant Design colors every ancestor submenu title in the selected path.");
secondLevelContainer.IsInSelectedPath.ShouldBeTrue(
"Every inline ancestor must expose selected-path state to its header.");
leafContainer.IsSelected.ShouldBeTrue();
var firstLevelHeader = GetItemHeader(firstLevelContainer);
var secondLevelHeader = GetItemHeader(secondLevelContainer);
firstLevelHeader.IsInSelectedPath.ShouldBeTrue(
"The selected-path state must reach the first ancestor header so its icon, text and arrow use the selected color.");
secondLevelHeader.IsInSelectedPath.ShouldBeTrue(
"Nested ancestor headers must receive selected-path state consistently.");
}
finally
{
window.Close();
}
}
[Fact]
public void Root_Group_Leaf_Selection_Uses_The_Root_Semantic_Selection_Owner()
{
var leaf = new NavMenuNode
{
Header = "Leaf",
ItemKey = "leaf"
};
var group = new NavMenuGroup { Header = "Section" };
group.Entries.Add(leaf);
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = false
};
menu.Items.Add(group);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var groupContainer = menu.ContainerFromItem(group).ShouldBeOfType<NavMenuGroupItem>();
var leafContainer = groupContainer.ContainerFromItem(leaf).ShouldBeOfType<NavMenuItem>();
menu.InteractionHandler.ShouldNotBeNull();
menu.SelectNavMenuItem(leafContainer);
leafContainer.IsSelected.ShouldBeTrue();
menu.SelectedItem.ShouldBeSameAs(leaf);
}
finally
{
window.Close();
}
}
[Fact]
public void Inline_Selection_Through_A_Group_Uses_The_Closest_Node_Selected_Path()
{
var leaf = new NavMenuNode
{
Header = "Leaf",
ItemKey = "leaf"
};
var group = new NavMenuGroup { Header = "Section" };
group.Entries.Add(leaf);
var parent = new NavMenuNode
{
Header = "Parent",
ItemKey = "parent"
};
parent.Entries.Add(group);
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = false
};
menu.Items.Add(parent);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var parentContainer = menu.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
parentContainer.Open();
Dispatcher.UIThread.RunJobs();
var groupContainer = parentContainer.ContainerFromItem(group).ShouldBeOfType<NavMenuGroupItem>();
var leafContainer = groupContainer.ContainerFromItem(leaf).ShouldBeOfType<NavMenuItem>();
menu.InteractionHandler.ShouldNotBeNull();
menu.SelectNavMenuItem(leafContainer);
parentContainer.IsInSelectedPath.ShouldBeTrue();
leafContainer.IsSelected.ShouldBeTrue();
menu.SelectedItem.ShouldBeSameAs(leaf);
}
finally
{
window.Close();
}
}
[Fact]
public void Inline_Selected_Path_Submenu_Header_Keeps_Selected_Foreground_When_Hovered()
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = false
};
var leaf = new NavMenuNode
{
Header = "Option 1",
ItemKey = "option-1"
};
var parent = new NavMenuNode
{
Header = "Navigation Three - Submenu",
ItemKey = "navigation-three"
};
parent.Children.Add(leaf);
menu.Items.Add(parent);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var parentContainer = (NavMenuItem)menu.ContainerFromItem(parent)!;
parentContainer.Open();
Dispatcher.UIThread.RunJobs();
var leafContainer = (NavMenuItem)parentContainer.ContainerFromItem(leaf)!;
menu.InteractionHandler.ShouldNotBeNull();
menu.SelectNavMenuItem(leafContainer);
Dispatcher.UIThread.RunJobs();
var parentHeader = GetItemHeader(parentContainer);
var selectedForegroundBefore = GetSolidBrushColor(parentHeader.Foreground);
selectedForegroundBefore.ShouldNotBeNull();
parentHeader.IsInSelectedPath.ShouldBeTrue();
MouseMove(parentHeader, window);
Dispatcher.UIThread.RunJobs();
GetSolidBrushColor(parentHeader.Foreground).ShouldBe(
selectedForegroundBefore,
"hover must not override the selected-path submenu title foreground; Ant Design keeps selected ancestor submenu titles in the selected color.");
}
finally
{
window.Close();
}
}
[Fact]
public void Inline_Item_Background_Control_Propagates_To_Header_And_Submenu_Frame()
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = false,
IsItemBackgroundEnabled = false
};
var grandchild = new NavMenuNode
{
Header = "Grandchild",
ItemKey = "grandchild"
};
var child = new NavMenuNode
{
Header = "Child",
ItemKey = "child"
};
child.Children.Add(grandchild);
var parent = new NavMenuNode
{
Header = "Parent",
ItemKey = "parent"
};
parent.Children.Add(child);
menu.Items.Add(parent);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
var parentMenuItem = (INavMenuItem)parentContainer;
parentMenuItem.Open();
Dispatcher.UIThread.RunJobs();
var childContainer = (NavMenuItem)((ItemsControl)parentContainer).ContainerFromItem(child)!;
var childMenuItem = (INavMenuItem)childContainer;
childMenuItem.Open();
Dispatcher.UIThread.RunJobs();
var grandchildContainer = (NavMenuItem)childContainer.ContainerFromItem(grandchild)!;
var parentHeader = GetItemHeader(parentContainer);
var childHeader = GetItemHeader(childContainer);
var grandchildHeader = GetItemHeader(grandchildContainer);
var childFrame = GetChildItemsFrame(parentContainer);
var grandchildFrame = GetChildItemsFrame(childContainer);
parentHeader.IsItemBackgroundEnabled.ShouldBeFalse();
childHeader.IsItemBackgroundEnabled.ShouldBeFalse(
"the menu-level background control must be inherited by nested inline menu items.");
grandchildHeader.IsItemBackgroundEnabled.ShouldBeFalse(
"every realized descendant header must receive the same background control value.");
GetSolidBrushColor(parentHeader.Background).ShouldBe(Colors.Transparent);
GetSolidBrushColor(childFrame.Background).ShouldBe(Colors.Transparent);
GetSolidBrushColor(grandchildFrame.Background).ShouldBe(Colors.Transparent);
MouseMove(grandchildHeader, window);
Dispatcher.UIThread.RunJobs();
GetSolidBrushColor(grandchildHeader.Background).ShouldNotBe(
Colors.Transparent,
"NavMenuItemHeader hover visuals must stay available when NavMenuItem background is disabled.");
menu.InteractionHandler.ShouldNotBeNull();
menu.SelectNavMenuItem(grandchildContainer);
Dispatcher.UIThread.RunJobs();
GetSolidBrushColor(grandchildHeader.Background).ShouldNotBe(
Colors.Transparent,
"NavMenuItemHeader selected visuals must stay available when NavMenuItem background is disabled.");
menu.IsItemBackgroundEnabled = true;
Dispatcher.UIThread.RunJobs();
parentHeader.IsItemBackgroundEnabled.ShouldBeTrue();
childHeader.IsItemBackgroundEnabled.ShouldBeTrue();
grandchildHeader.IsItemBackgroundEnabled.ShouldBeTrue();
}
finally
{
window.Close();
}
}
[Theory]
[InlineData(NavMenuMode.Horizontal)]
[InlineData(NavMenuMode.Vertical)]
[InlineData(NavMenuMode.Inline)]
public void Selecting_Realized_Item_Does_Not_Temporarily_Disable_Motion(NavMenuMode mode)
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = mode,
IsMotionEnabled = true
};
var item = new NavMenuNode
{
Header = "Item",
ItemKey = "item"
};
menu.Items.Add(item);
var disabledDuringSelection = false;
menu.PropertyChanged += (_, args) =>
{
if (args.Property == AtomUI.Desktop.Controls.NavMenu.IsMotionEnabledProperty &&
args.NewValue is false)
{
disabledDuringSelection = true;
}
};
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var itemContainer = (NavMenuItem)menu.ContainerFromItem(item)!;
menu.InteractionHandler.ShouldNotBeNull();
menu.SelectNavMenuItem(itemContainer);
Dispatcher.UIThread.RunJobs();
disabledDuringSelection.ShouldBeFalse(
"a user item selection is already applied by the interaction handler and must not replay the path by disabling motion");
}
finally
{
window.Close();
}
}
[Fact]
public void Inline_Submenu_Open_With_Motion_Disabled_Restores_Actor_Opacity_After_Animated_Close()
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = true
};
var parent = new NavMenuNode
{
Header = "Parent",
ItemKey = "parent"
};
parent.Children.Add(new NavMenuNode
{
Header = "Child",
ItemKey = "child"
});
menu.Items.Add(parent);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
SetTimeSpanProperty(parentContainer, "OpenCloseMotionDuration", TimeSpan.FromMilliseconds(1));
var actor = GetChildItemsMotionActor(parentContainer);
var parentMenuItem = (INavMenuItem)parentContainer;
parentMenuItem.Open();
WaitForInlineMotionCompletion(actor);
parentMenuItem.Close();
WaitForInlineMotionCompletion(actor);
actor.IsVisible.ShouldBeFalse();
actor.Opacity.ShouldBe(0.0);
menu.IsMotionEnabled = false;
DrainDispatcher();
parentMenuItem.Open();
DrainDispatcher();
actor.IsVisible.ShouldBeTrue();
actor.Opacity.ShouldBe(1.0);
}
finally
{
window.Close();
}
}
[Fact]
public void Inline_Submenu_Open_With_Motion_Disabled_Clears_Child_Header_Transitions_After_Animated_Close()
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = true
};
var child = new NavMenuNode
{
Header = "Child",
ItemKey = "child"
};
var parent = new NavMenuNode
{
Header = "Parent",
ItemKey = "parent"
};
parent.Children.Add(child);
menu.Items.Add(parent);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
SetTimeSpanProperty(parentContainer, "OpenCloseMotionDuration", TimeSpan.FromMilliseconds(1));
var parentMenuItem = (INavMenuItem)parentContainer;
parentMenuItem.Open();
DrainDispatcher();
var childContainer = (Control)((ItemsControl)parentContainer).ContainerFromItem(child)!;
var childHeader = GetItemHeader(childContainer);
childHeader.IsMotionEnabled.ShouldBeTrue();
childHeader.Transitions.ShouldNotBeNull();
parentMenuItem.Close();
DrainDispatcher();
menu.IsMotionEnabled = false;
DrainDispatcher();
parentMenuItem.Open();
DrainDispatcher();
childHeader.IsMotionEnabled.ShouldBeFalse();
childHeader.Transitions.ShouldBeNull();
}
finally
{
window.Close();
}
}
[Fact]
public void Inline_Submenu_Open_With_Motion_Disabled_Overrides_Pending_Animated_Close()
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = true
};
var parent = new NavMenuNode
{
Header = "Parent",
ItemKey = "parent"
};
parent.Children.Add(new NavMenuNode
{
Header = "Child",
ItemKey = "child"
});
menu.Items.Add(parent);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
var parentMenuItem = (INavMenuItem)parentContainer;
SetTimeSpanProperty(parentContainer, "OpenCloseMotionDuration", TimeSpan.FromMilliseconds(1));
parentMenuItem.Open();
WaitForInlineMotionCompletion(GetChildItemsMotionActor(parentContainer));
SetTimeSpanProperty(parentContainer, "OpenCloseMotionDuration", TimeSpan.FromMilliseconds(500));
parentMenuItem.Close();
Dispatcher.UIThread.RunJobs();
Dispatcher.UIThread.RunJobs();
var actor = GetChildItemsMotionActor(parentContainer);
actor.IsVisible.ShouldBeTrue();
actor.IsAnimating(Visual.OpacityProperty).ShouldBeTrue();
menu.IsMotionEnabled = false;
Dispatcher.UIThread.RunJobs();
parentMenuItem.Open();
Dispatcher.UIThread.RunJobs();
parentMenuItem.IsSubMenuOpen.ShouldBeTrue();
actor.IsAnimating(Visual.OpacityProperty).ShouldBeFalse();
Thread.Sleep(600);
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
Dispatcher.UIThread.RunJobs();
actor.IsVisible.ShouldBeTrue();
actor.Opacity.ShouldBe(1.0);
}
finally
{
window.Close();
}
}
[Fact]
public void SelectedItem_Is_Already_Updated_When_NodeSelected_Event_Raises()
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = false
};
var first = new NavMenuNode { Header = "First", ItemKey = "first" };
var second = new NavMenuNode { Header = "Second", ItemKey = "second" };
menu.Items.Add(first);
menu.Items.Add(second);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var secondContainer = menu.ContainerFromItem(second).ShouldBeOfType<NavMenuItem>();
INavMenuNode? observed = null;
menu.NavMenuNodeSelected += (_, _) => observed = menu.SelectedItem;
menu.SelectNavMenuItem(secondContainer);
observed.ShouldBeSameAs(second,
"NavMenuNodeSelected subscribers must observe the committed SelectedItem.");
menu.SelectedItem.ShouldBeSameAs(second);
}
finally
{
window.Close();
}
}
private static bool IsSelected(Control container)
{
return GetBoolProperty(container, "IsSelected");
}
private static bool GetBoolProperty(Control container, string propertyName)
{
var property = container.GetType().GetProperty(
propertyName,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
property.ShouldNotBeNull();
return (bool)property.GetValue(container)!;
}
private static BaseMotionActor GetChildItemsMotionActor(Control container)
{
var field = container.GetType().GetField(
"_childItemsLayoutTransform",
BindingFlags.Instance | BindingFlags.NonPublic);
field.ShouldNotBeNull();
return (BaseMotionActor)field.GetValue(container)!;
}
private static Border GetChildItemsFrame(Control container)
{
var actor = GetChildItemsMotionActor(container);
actor.Content.ShouldBeOfType<Border>();
return (Border)actor.Content;
}
private static BaseNavMenuItemHeader GetItemHeader(Control container)
{
var field = container.GetType().GetField(
"_itemHeader",
BindingFlags.Instance | BindingFlags.NonPublic);
field.ShouldNotBeNull();
return (BaseNavMenuItemHeader)field.GetValue(container)!;
}
private static void SetTimeSpanProperty(Control container, string propertyName, TimeSpan value)
{
var property = container.GetType().GetProperty(
propertyName,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
property.ShouldNotBeNull();
property.SetValue(container, value);
}
private static void MouseDown(Control control, Avalonia.Controls.Window window)
{
MouseMove(control, window);
window.MouseDown(GetCenterPoint(control, window), MouseButton.Left);
}
private static void MouseMove(Control control, Avalonia.Controls.Window window)
{
window.MouseMove(GetCenterPoint(control, window));
}
private static Point GetCenterPoint(Control control, Avalonia.Controls.Window window)
{
var point = control.TranslatePoint(
new Point(control.Bounds.Width / 2, control.Bounds.Height / 2),
window);
point.ShouldNotBeNull();
return point.Value;
}
private static Color? GetSolidBrushColor(IBrush? brush)
{
return brush is ISolidColorBrush solidColorBrush
? solidColorBrush.Color
: null;
}
private static void WaitForInlineMotionCompletion(BaseMotionActor actor)
{
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
do
{
Dispatcher.UIThread.RunJobs();
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
Dispatcher.UIThread.RunJobs();
if (!actor.IsAnimating(Visual.OpacityProperty))
{
return;
}
Thread.Sleep(10);
} while (DateTime.UtcNow < deadline);
actor.IsAnimating(Visual.OpacityProperty).ShouldBeFalse();
}
private static void DrainDispatcher()
{
Dispatcher.UIThread.RunJobs();
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
Thread.Sleep(20);
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
Dispatcher.UIThread.RunJobs();
}
private static void RunDispatcherJobsUntil(Func<bool> condition, int maxPasses = 128)
{
for (var i = 0; i < maxPasses && !condition(); i++)
{
Dispatcher.UIThread.RunJobs();
}
}
private static Avalonia.Controls.Primitives.VisualLayerManager CreatePopupOverlayHost(Control content)
{
var visualLayerManager = new Avalonia.Controls.Primitives.VisualLayerManager
{
Child = content
};
EnablePopupOverlayLayer(visualLayerManager);
return visualLayerManager;
}
private static void EnablePopupOverlayLayer(Avalonia.Controls.Primitives.VisualLayerManager visualLayerManager)
{
var property = typeof(Avalonia.Controls.Primitives.VisualLayerManager).GetProperty(
"EnablePopupOverlayLayer",
BindingFlags.Instance | BindingFlags.NonPublic);
property.ShouldNotBeNull();
property.SetValue(visualLayerManager, true);
}
}