Files
AtomUI/tests/AtomUI.Desktop.Controls.Tests/NavMenu/NavMenuLayoutTests.cs
T

445 lines
14 KiB
C#

using System.Reflection;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Headless;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Shouldly;
using Xunit;
namespace AtomUI.Desktop.Controls.Tests.NavMenu;
public class NavMenuLayoutTests
{
static NavMenuLayoutTests()
{
AvaloniaTestApp.EnsureInitialized();
}
[Theory]
[InlineData(NavMenuMode.Inline)]
[InlineData(NavMenuMode.Vertical)]
public void Root_Item_Inset_Uses_AntDesign_Item_Margins_Without_Outer_Menu_Padding(NavMenuMode mode)
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = mode,
IsMotionEnabled = false,
Width = 240
};
var item = new NavMenuNode
{
Header = "Item",
ItemKey = "item"
};
menu.Items.Add(item);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 240,
Content = menu
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var itemContainer = (Control)menu.ContainerFromItem(item)!;
var itemHeader = GetItemHeader(itemContainer);
var leftInset = GetLeft(itemHeader, menu);
var rightInset = menu.Bounds.Width - GetRight(itemHeader, menu);
var topInset = GetTop(itemHeader, menu);
leftInset.ShouldBe(4, 0.5,
"Ant Design vertical/inline root menus do not add outer menu padding; item marginInline is the only horizontal inset.");
rightInset.ShouldBe(4, 0.5,
"Ant Design vertical/inline root menus do not add outer menu padding; item marginInline is the only horizontal inset.");
topInset.ShouldBe(4, 0.5,
"Ant Design vertical/inline root menus expose one marginXXS before the first item.");
}
finally
{
window.Close();
}
}
[Theory]
[InlineData(NavMenuMode.Inline)]
[InlineData(NavMenuMode.Vertical)]
public void Vertical_Item_Header_Gap_Collapses_AntDesign_Block_Margins(NavMenuMode mode)
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = mode,
IsMotionEnabled = false,
Width = 240
};
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();
var firstContainer = (Control)menu.ContainerFromItem(first)!;
var secondContainer = (Control)menu.ContainerFromItem(second)!;
var firstHeader = GetItemHeader(firstContainer);
var secondHeader = GetItemHeader(secondContainer);
var gap = GetTop(secondHeader, menu) - GetBottom(firstHeader, menu);
gap.ShouldBe(4, 0.5,
"Ant Design uses marginBlock: marginXXS on adjacent block menu items; CSS collapses those adjacent vertical margins to 4px.");
}
finally
{
window.Close();
}
}
[Fact]
public void Inline_Submenu_First_Child_Header_Gap_Collapses_AntDesign_Block_Margins()
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = false,
Width = 240
};
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 = 320,
Content = menu
};
try
{
window.Show();
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
var parentMenuItem = (INavMenuItem)parentContainer;
parentMenuItem.Open();
Dispatcher.UIThread.RunJobs();
var parentHeader = GetItemHeader(parentContainer);
var childContainer = (Control)((ItemsControl)parentContainer).ContainerFromItem(child)!;
var childHeader = GetItemHeader(childContainer);
var gap = GetTop(childHeader, menu) - GetBottom(parentHeader, menu);
gap.ShouldBe(4, 0.5,
"Ant Design does not add an extra top margin to the inline child items container.");
}
finally
{
window.Close();
}
}
[Fact]
public void Inline_Submenu_Child_Header_Gap_Collapses_AntDesign_Block_Margins()
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = false,
Width = 240
};
var firstChild = new NavMenuNode
{
Header = "First child",
ItemKey = "first-child"
};
var secondChild = new NavMenuNode
{
Header = "Second child",
ItemKey = "second-child"
};
var parent = new NavMenuNode
{
Header = "Parent",
ItemKey = "parent"
};
parent.Children.Add(firstChild);
parent.Children.Add(secondChild);
menu.Items.Add(parent);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 320,
Content = menu
};
try
{
window.Show();
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
var parentMenuItem = (INavMenuItem)parentContainer;
parentMenuItem.Open();
Dispatcher.UIThread.RunJobs();
var firstChildContainer = (Control)((ItemsControl)parentContainer).ContainerFromItem(firstChild)!;
var secondChildContainer = (Control)((ItemsControl)parentContainer).ContainerFromItem(secondChild)!;
var firstChildHeader = GetItemHeader(firstChildContainer);
var secondChildHeader = GetItemHeader(secondChildContainer);
var gap = GetTop(secondChildHeader, menu) - GetBottom(firstChildHeader, menu);
gap.ShouldBe(4, 0.5,
"Inline submenu children share Ant Design's vertical menu item margin semantics.");
}
finally
{
window.Close();
}
}
[Theory]
[InlineData(true, 4d)]
[InlineData(false, 0d)]
public void Inline_Submenu_Background_Gap_Follows_Item_Background_Mode(
bool isItemBackgroundEnabled,
double expectedGap)
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = NavMenuMode.Inline,
IsMotionEnabled = false,
IsItemBackgroundEnabled = isItemBackgroundEnabled,
Width = 240
};
var firstChild = new NavMenuNode
{
Header = "First child",
ItemKey = "first-child"
};
var secondChild = new NavMenuNode
{
Header = "Second child",
ItemKey = "second-child"
};
var parent = new NavMenuNode
{
Header = "Parent",
ItemKey = "parent"
};
var nextRoot = new NavMenuNode
{
Header = "Next root",
ItemKey = "next-root"
};
parent.Children.Add(firstChild);
parent.Children.Add(secondChild);
menu.Items.Add(parent);
menu.Items.Add(nextRoot);
var window = new Avalonia.Controls.Window
{
Width = 320,
Height = 360,
Content = menu
};
try
{
window.Show();
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
var parentMenuItem = (INavMenuItem)parentContainer;
parentMenuItem.Open();
menu.SelectedItem = secondChild;
Dispatcher.UIThread.RunJobs();
var childItemsFrame = FindChildItemsFrame(parentContainer);
var nextRootContainer = (Control)menu.ContainerFromItem(nextRoot)!;
var nextRootHeader = GetItemHeader(nextRootContainer);
var gap = GetTop(nextRootHeader, menu) - GetBottom(childItemsFrame, menu);
gap.ShouldBe(expectedGap, 0.5,
"The extra inline submenu block gap is only needed when the submenu background block is visible.");
}
finally
{
window.Close();
}
}
[Theory]
[InlineData(NavMenuMode.Vertical)]
[InlineData(NavMenuMode.Horizontal)]
public void Popup_Item_Inset_Uses_AntDesign_Item_Margins(NavMenuMode mode)
{
var menu = new AtomUI.Desktop.Controls.NavMenu
{
Mode = mode,
IsMotionEnabled = false,
Width = 240
};
var firstChild = new NavMenuNode
{
Header = "Item 1",
ItemKey = "item-1"
};
var secondChild = new NavMenuNode
{
Header = "Item 2",
ItemKey = "item-2"
};
var parent = new NavMenuNode
{
Header = "Parent",
ItemKey = "parent"
};
parent.Children.Add(firstChild);
parent.Children.Add(secondChild);
menu.Items.Add(parent);
var window = new Avalonia.Controls.Window
{
Width = 640,
Height = 480,
Content = CreatePopupOverlayHost(menu)
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
var parentContainer = (Control)menu.ContainerFromItem(parent)!;
var parentMenuItem = (INavMenuItem)parentContainer;
parentMenuItem.Open();
Dispatcher.UIThread.RunJobs();
var popupFrame = FindPopupFrame(window);
var firstChildContainer = (Control)((ItemsControl)parentContainer).ContainerFromItem(firstChild)!;
var secondChildContainer = (Control)((ItemsControl)parentContainer).ContainerFromItem(secondChild)!;
var firstChildHeader = GetItemHeader(firstChildContainer);
var secondChildHeader = GetItemHeader(secondChildContainer);
var leftInset = GetLeft(firstChildHeader, popupFrame);
var rightInset = popupFrame.Bounds.Width - GetRight(firstChildHeader, popupFrame);
var topInset = GetTop(firstChildHeader, popupFrame);
var bottomInset = popupFrame.Bounds.Height - GetBottom(secondChildHeader, popupFrame);
leftInset.ShouldBe(4, 0.5,
"Ant Design popup submenus use the item's marginInline as the only horizontal inset.");
rightInset.ShouldBe(4, 0.5,
"Ant Design popup submenus use the item's marginInline as the only horizontal inset.");
topInset.ShouldBe(4, 0.5,
"Ant Design popup submenus expose one marginXXS before the first item.");
bottomInset.ShouldBe(4, 0.5,
"Ant Design popup submenus expose one marginXXS after the last item.");
}
finally
{
window.Close();
}
}
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 double GetTop(Control control, Visual relativeTo)
{
var point = control.TranslatePoint(default, relativeTo);
point.ShouldNotBeNull();
return point.Value.Y;
}
private static double GetLeft(Control control, Visual relativeTo)
{
var point = control.TranslatePoint(default, relativeTo);
point.ShouldNotBeNull();
return point.Value.X;
}
private static double GetRight(Control control, Visual relativeTo)
{
return GetLeft(control, relativeTo) + control.Bounds.Width;
}
private static double GetBottom(Control control, Visual relativeTo)
{
return GetTop(control, relativeTo) + control.Bounds.Height;
}
private static Border FindPopupFrame(Visual root)
{
return root.GetVisualDescendants()
.OfType<Border>()
.Single(border => border.Name == "PART_PopupFrame");
}
private static Border FindChildItemsFrame(Visual root)
{
return root.GetVisualDescendants()
.OfType<Border>()
.Single(border => border.Name == "PART_ChildItemsFrame");
}
private static VisualLayerManager CreatePopupOverlayHost(Control content)
{
var visualLayerManager = new VisualLayerManager
{
Child = content
};
EnablePopupOverlayLayer(visualLayerManager);
return visualLayerManager;
}
private static void EnablePopupOverlayLayer(VisualLayerManager visualLayerManager)
{
var property = typeof(VisualLayerManager).GetProperty(
"EnablePopupOverlayLayer",
BindingFlags.Instance | BindingFlags.NonPublic);
property.ShouldNotBeNull();
property.SetValue(visualLayerManager, true);
}
}