mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-20 23:58:05 +08:00
568 lines
24 KiB
C#
568 lines
24 KiB
C#
using System.Reflection;
|
|
using System.Xml.Linq;
|
|
using AtomUI.Controls;
|
|
using AtomUI.Theme;
|
|
using AtomUI.Theme.Schema;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Presenters;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Styling;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AtomUINavMenu = AtomUI.Desktop.Controls.NavMenu;
|
|
using AtomUIWindow = AtomUI.Desktop.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.NavMenu;
|
|
|
|
public class NavMenuSemanticPartTests
|
|
{
|
|
private const string ItemClass = "semantic-item";
|
|
private const string ItemIconClass = "semantic-item-icon";
|
|
private const string ItemContentClass = "semantic-item-content";
|
|
private const string ItemTitleClass = "semantic-item-title";
|
|
private const string ListClass = "semantic-list";
|
|
private const string SubMenuItemClass = "semantic-sub-menu-item";
|
|
private const string SubMenuItemIconClass = "semantic-sub-menu-item-icon";
|
|
private const string SubMenuItemContentClass = "semantic-sub-menu-item-content";
|
|
private const string SubMenuItemTitleClass = "semantic-sub-menu-item-title";
|
|
private const string SubMenuListClass = "semantic-sub-menu-list";
|
|
private const string GroupScopeClass = "semantic-scope-group";
|
|
private const string SubMenuGroupScopeClass = "semantic-sub-menu-group";
|
|
private const string PopupRootClass = "semantic-popup-root";
|
|
private const string HeaderHop = "/template/ .semantic-scope-header /template/ .";
|
|
|
|
static NavMenuSemanticPartTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void Registered_Descriptor_Exposes_Only_The_Upstream_Key_Paths()
|
|
{
|
|
var registry = Application.Current.ShouldNotBeNull()
|
|
.GetThemeManager().ShouldNotBeNull()
|
|
.SemanticParts;
|
|
|
|
registry.TryGetControl(typeof(AtomUINavMenu), out var descriptor).ShouldBeTrue();
|
|
descriptor.ShouldNotBeNull();
|
|
descriptor.Parts.Select(static part => part.Name)
|
|
.ShouldBe(
|
|
[
|
|
"root",
|
|
"item",
|
|
"itemContent",
|
|
"itemIcon",
|
|
"itemTitle",
|
|
"list",
|
|
"popup.root",
|
|
"subMenu.item",
|
|
"subMenu.itemContent",
|
|
"subMenu.itemIcon",
|
|
"subMenu.itemTitle",
|
|
"subMenu.list"
|
|
]);
|
|
|
|
AssertRoot(descriptor.Parts.Single(static part => part.Name == "root"));
|
|
|
|
AssertRuntimePart(descriptor, "item", ItemClass,
|
|
$">> .{ItemClass}",
|
|
typeof(HeaderedSelectingItemsControl));
|
|
AssertRuntimePart(descriptor, "itemIcon", ItemIconClass,
|
|
$">> .{ItemClass} {HeaderHop}{ItemIconClass}",
|
|
typeof(IconPresenter));
|
|
AssertRuntimePart(descriptor, "itemContent", ItemContentClass,
|
|
$">> .{ItemClass} {HeaderHop}{ItemContentClass}",
|
|
typeof(ContentPresenter));
|
|
AssertRuntimePart(descriptor, "itemTitle", ItemTitleClass,
|
|
$">> .{GroupScopeClass} /template/ .{ItemTitleClass}",
|
|
typeof(ContentPresenter));
|
|
AssertRuntimePart(descriptor, "list", ListClass,
|
|
$">> .{GroupScopeClass} /template/ .{ListClass}",
|
|
typeof(ItemsPresenter));
|
|
|
|
AssertRuntimePart(descriptor, "subMenu.item", SubMenuItemClass,
|
|
$">> .{SubMenuItemClass}",
|
|
typeof(HeaderedSelectingItemsControl),
|
|
crossVisualRoot: true);
|
|
AssertRuntimePart(descriptor, "subMenu.itemIcon", SubMenuItemIconClass,
|
|
$">> .{SubMenuItemClass} {HeaderHop}{SubMenuItemIconClass}",
|
|
typeof(IconPresenter),
|
|
crossVisualRoot: true);
|
|
AssertRuntimePart(descriptor, "subMenu.itemContent", SubMenuItemContentClass,
|
|
$">> .{SubMenuItemClass} {HeaderHop}{SubMenuItemContentClass}",
|
|
typeof(ContentPresenter),
|
|
crossVisualRoot: true);
|
|
AssertRuntimePart(descriptor, "subMenu.itemTitle", SubMenuItemTitleClass,
|
|
$">> .{SubMenuGroupScopeClass} /template/ .{SubMenuItemTitleClass}",
|
|
typeof(ContentPresenter),
|
|
crossVisualRoot: true);
|
|
AssertRuntimePart(descriptor, "subMenu.list", SubMenuListClass,
|
|
$">> .{SubMenuGroupScopeClass} /template/ .{SubMenuListClass}",
|
|
typeof(ItemsPresenter),
|
|
crossVisualRoot: true);
|
|
|
|
var popupRoot = descriptor.Parts.Single(static part => part.Name == "popup.root");
|
|
popupRoot.Path.ShouldBe("popup.root");
|
|
popupRoot.SelectorClass.ShouldBe(PopupRootClass);
|
|
popupRoot.SelectorRoute.ShouldBe($">> .{PopupRootClass}");
|
|
popupRoot.ContractType.ShouldBe(typeof(Border));
|
|
popupRoot.Cardinality.ShouldBe(SemanticPartCardinality.Multiple);
|
|
popupRoot.Customization.ShouldBe(SemanticPartCustomization.Selector);
|
|
popupRoot.CrossVisualRoot.ShouldBeTrue();
|
|
popupRoot.CrossNestedOwners.ShouldBeTrue();
|
|
popupRoot.RuntimeCreated.ShouldBeTrue();
|
|
popupRoot.Since.ShouldBe("6.2.0");
|
|
|
|
// The item / group containers are internal, so they cannot own a descriptor.
|
|
registry.TryGetControl(typeof(NavMenuItem), out _).ShouldBeFalse();
|
|
registry.TryGetControl(typeof(NavMenuGroupItem), out _).ShouldBeFalse();
|
|
registry.TryGetControl(typeof(NavMenuDividerItem), out _).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Templates_Implement_Only_The_Approved_Static_Markers()
|
|
{
|
|
AssertThemeMarkers(
|
|
"src/AtomUI.Desktop.Controls/NavMenu/Themes/NavMenuTheme.axaml",
|
|
Array.Empty<string>());
|
|
AssertThemeMarkers(
|
|
"src/AtomUI.Desktop.Controls/NavMenu/Themes/BaseNavMenuItemHeaderTheme.axaml",
|
|
Array.Empty<string>());
|
|
AssertThemeMarkers(
|
|
"src/AtomUI.Desktop.Controls/NavMenu/Themes/NavMenuItemTheme.axaml",
|
|
[
|
|
"semantic-popup-root:NavMenuPopupFrame",
|
|
"semantic-popup-root:NavMenuPopupFrame",
|
|
"semantic-scope-header:HorizontalNavMenuItemHeader",
|
|
"semantic-scope-header:InlineNavMenuItemHeader",
|
|
"semantic-scope-header:VerticalNavMenuItemHeader"
|
|
]);
|
|
|
|
string[] headerMarkers =
|
|
[
|
|
$"{ItemContentClass}:ContentPresenter",
|
|
$"{ItemIconClass}:IconPresenter",
|
|
$"{SubMenuItemContentClass}:ContentPresenter",
|
|
$"{SubMenuItemIconClass}:IconPresenter"
|
|
];
|
|
AssertThemeMarkers(
|
|
"src/AtomUI.Desktop.Controls/NavMenu/Themes/HorizontalNavMenuItemHeaderTheme.axaml",
|
|
headerMarkers);
|
|
AssertThemeMarkers(
|
|
"src/AtomUI.Desktop.Controls/NavMenu/Themes/VerticalNavMenuItemHeaderTheme.axaml",
|
|
headerMarkers);
|
|
AssertThemeMarkers(
|
|
"src/AtomUI.Desktop.Controls/NavMenu/Themes/InlineNavMenuItemHeaderTheme.axaml",
|
|
headerMarkers);
|
|
|
|
AssertThemeMarkers(
|
|
"src/AtomUI.Desktop.Controls/NavMenu/Themes/NavMenuGroupItemTheme.axaml",
|
|
[
|
|
$"{ItemTitleClass}:ContentPresenter",
|
|
$"{ListClass}:ItemsPresenter",
|
|
$"{SubMenuItemTitleClass}:ContentPresenter",
|
|
$"{SubMenuListClass}:ItemsPresenter"
|
|
]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Menu_Marks_First_Level_And_Submenu_Containers_By_Level()
|
|
{
|
|
var (menu, root, subMenuNode) = CreateMenuWithSubmenu(NavMenuMode.Inline);
|
|
|
|
ShowInWindow(menu, root, host =>
|
|
{
|
|
var subMenuContainer = OpenSubMenu(menu, subMenuNode);
|
|
|
|
// First level: the two leaf containers plus the leaf inside the top-level group.
|
|
Marked(host, ItemClass).Length.ShouldBe(3);
|
|
Marked(host, GroupScopeClass).Length.ShouldBe(1);
|
|
Marked(host, ItemTitleClass).Length.ShouldBe(2);
|
|
Marked(host, ListClass).Length.ShouldBe(2);
|
|
|
|
// Submenu level: the nested leaf and the leaf inside the submenu's group.
|
|
Marked(host, SubMenuItemClass).Length.ShouldBe(2);
|
|
Marked(host, SubMenuGroupScopeClass).Length.ShouldBe(1);
|
|
Marked(host, SubMenuItemTitleClass).Length.ShouldBe(2);
|
|
Marked(host, SubMenuListClass).Length.ShouldBe(2);
|
|
|
|
// Depth isolation: a first-level container never carries the submenu identity.
|
|
Marked<NavMenuItem>(host, ItemClass).ShouldContain(subMenuContainer);
|
|
Marked<NavMenuItem>(host, SubMenuItemClass).ShouldNotContain(subMenuContainer);
|
|
|
|
// Inline mode expands inside the visual tree, so there is no popup layer.
|
|
Marked(host, PopupRootClass).Length.ShouldBe(0);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Vertical_Popup_Marks_Submenu_Containers_And_Popup_Root()
|
|
{
|
|
var (menu, root, subMenuNode) = CreateMenuWithSubmenu(NavMenuMode.Vertical);
|
|
|
|
ShowInWindow(menu, root, host =>
|
|
{
|
|
// Pinning realizes the popup subtree, including the nested containers.
|
|
var subMenuContainer = menu.ContainerFromItem(subMenuNode).ShouldBeOfType<NavMenuItem>();
|
|
|
|
Marked(host, ItemClass).Length.ShouldBe(3);
|
|
Marked(host, SubMenuItemClass).Length.ShouldBe(2);
|
|
Marked<NavMenuItem>(host, SubMenuItemClass).ShouldNotContain(subMenuContainer);
|
|
|
|
// Horizontal / Vertical templates own the popup frame node.
|
|
Marked(host, PopupRootClass).Length.ShouldBeGreaterThanOrEqualTo(1);
|
|
|
|
// Closing and reopening the popup branch does not change the popup frame marker count:
|
|
// the frame is part of the item template, not of the popup's realized content.
|
|
var popupCount = Marked(host, PopupRootClass).Length;
|
|
menu.IsPopupPinnedOpen = false;
|
|
Dispatcher.UIThread.RunJobs();
|
|
Marked(host, PopupRootClass).Length.ShouldBe(popupCount);
|
|
|
|
menu.IsPopupPinnedOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
Marked(host, PopupRootClass).Length.ShouldBe(popupCount);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Submenu_Route_Does_Not_Match_First_Level_Containers()
|
|
{
|
|
var (menu, root, _) = CreateMenuWithSubmenu(NavMenuMode.Inline);
|
|
menu.Classes.Add("semantic-owner");
|
|
menu.Styles.Add(new Style(selector =>
|
|
selector.OfType<AtomUINavMenu>()
|
|
.Class("semantic-owner")
|
|
.Descendant()
|
|
.Class(SubMenuItemClass))
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "subMenu.item") }
|
|
});
|
|
|
|
ShowInWindow(menu, root, host =>
|
|
{
|
|
Marked<NavMenuItem>(host, SubMenuItemClass)
|
|
.ShouldAllBe(static item => Equals(item.Tag, "subMenu.item"));
|
|
Marked<NavMenuItem>(host, ItemClass).ShouldAllBe(static item => Equals(item.Tag, null));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Expansion_And_Selection_Do_Not_Change_Marker_Counts()
|
|
{
|
|
var (menu, root, subMenuNode) = CreateMenuWithSubmenu(NavMenuMode.Inline);
|
|
|
|
ShowInWindow(menu, root, host =>
|
|
{
|
|
var subMenuContainer = OpenSubMenu(menu, subMenuNode);
|
|
var baseline = Marked(host, SubMenuItemClass).Length;
|
|
|
|
subMenuContainer.SetCurrentValue(NavMenuItem.IsSubMenuOpenProperty, false);
|
|
Dispatcher.UIThread.RunJobs();
|
|
Marked(host, SubMenuItemClass).Length.ShouldBe(baseline);
|
|
|
|
subMenuContainer.SetCurrentValue(NavMenuItem.IsSubMenuOpenProperty, true);
|
|
Dispatcher.UIThread.RunJobs();
|
|
Marked(host, SubMenuItemClass).Length.ShouldBe(baseline);
|
|
});
|
|
}
|
|
|
|
private static NavMenuItem OpenSubMenu(AtomUINavMenu menu, NavMenuNode subMenuNode)
|
|
{
|
|
var container = menu.ContainerFromItem(subMenuNode).ShouldBeOfType<NavMenuItem>();
|
|
container.SetCurrentValue(NavMenuItem.IsSubMenuOpenProperty, true);
|
|
Dispatcher.UIThread.RunJobs();
|
|
return container;
|
|
}
|
|
|
|
private static void AssertRuntimePart(
|
|
ControlSemanticDescriptor descriptor,
|
|
string name,
|
|
string selectorClass,
|
|
string selectorRoute,
|
|
Type contractType,
|
|
bool crossVisualRoot = false)
|
|
{
|
|
var part = descriptor.Parts.Single(candidate => candidate.Name == name);
|
|
part.Path.ShouldBe(name);
|
|
part.SelectorClass.ShouldBe(selectorClass);
|
|
part.SelectorRoute.ShouldBe(selectorRoute);
|
|
part.ContractType.ShouldBe(contractType);
|
|
part.Cardinality.ShouldBe(SemanticPartCardinality.Multiple);
|
|
part.Customization.ShouldBe(SemanticPartCustomization.Selector);
|
|
part.CrossVisualRoot.ShouldBe(crossVisualRoot);
|
|
part.CrossNestedOwners.ShouldBeTrue();
|
|
part.RuntimeCreated.ShouldBeTrue();
|
|
part.Since.ShouldBe("6.2.0");
|
|
}
|
|
|
|
private static void AssertRoot(SemanticPartDescriptor part)
|
|
{
|
|
part.Path.ShouldBe("root");
|
|
part.SelectorClass.ShouldBeNull();
|
|
part.ContractType.ShouldBe(typeof(AtomUINavMenu));
|
|
part.Cardinality.ShouldBe(SemanticPartCardinality.Single);
|
|
part.Customization.ShouldBe(SemanticPartCustomization.Root);
|
|
part.CrossVisualRoot.ShouldBeFalse();
|
|
part.RuntimeCreated.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Pinned_Open_Declared_Before_Containers_Exist_Still_Opens_Submenu()
|
|
{
|
|
// 复刻 Gallery 语义预览的用法:在容器生成前就声明钉住打开(AXAML 里
|
|
// IsPopupPinnedOpen="True" 会在模板应用、容器生成之前写入属性)。钉住请求以节点为键保存在
|
|
// NavMenu 上,独立于容器生命周期,因此模板应用与 attach 时都不需要容器存在。
|
|
var parent = new NavMenuNode { Header = "Parent" };
|
|
parent.Children.Add(new NavMenuNode { Header = "Child" });
|
|
|
|
var menu = new AtomUINavMenu
|
|
{
|
|
Mode = NavMenuMode.Vertical,
|
|
IsMotionEnabled = false,
|
|
ShouldUseOverlayPopup = true,
|
|
IsPopupPinnedOpen = true
|
|
};
|
|
menu.Items.Add(new NavMenuNode { Header = "Leaf" });
|
|
menu.Items.Add(parent);
|
|
|
|
var root = CreateRoot(menu);
|
|
var window = new AtomUIWindow { Width = 520, Height = 400, Content = root };
|
|
try
|
|
{
|
|
window.Show();
|
|
for (var i = 0; i < 5; i++)
|
|
{
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.UpdateLayout();
|
|
}
|
|
|
|
var container = menu.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
|
|
container.IsPopupPinnedOpen.ShouldBeTrue(
|
|
"a declarative pin must reach containers that were created after the property was set");
|
|
container.IsSubMenuOpen.ShouldBeTrue();
|
|
container.Popup.ShouldNotBeNull().IsOpen.ShouldBeTrue();
|
|
|
|
// 钉住语义:普通关闭请求不得收起弹层。
|
|
container.Close();
|
|
container.SetCurrentValue(NavMenuItem.IsSubMenuOpenProperty, false);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
container.IsSubMenuOpen.ShouldBeTrue();
|
|
container.Popup.ShouldNotBeNull().IsOpen.ShouldBeTrue();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Pinned_Open_Request_Survives_Container_Rebuild_And_Late_Items()
|
|
{
|
|
// 钉住请求必须是独立于容器的持久状态:Mode 切换会让 ItemsPresenter 回收并重建全部容器,
|
|
// 之后追加的条目也会新建容器。只要容器对应的节点仍在钉住路径上,重建后的容器必须重新带上
|
|
// 钉住并保持弹层打开,而不是因为“请求早已消费”而丢失。
|
|
var parent = new NavMenuNode { Header = "Parent" };
|
|
parent.Children.Add(new NavMenuNode { Header = "Child" });
|
|
|
|
var menu = new AtomUINavMenu
|
|
{
|
|
Mode = NavMenuMode.Vertical,
|
|
IsMotionEnabled = false,
|
|
ShouldUseOverlayPopup = true
|
|
};
|
|
menu.Items.Add(parent);
|
|
|
|
var root = CreateRoot(menu);
|
|
var window = new AtomUIWindow { Width = 520, Height = 400, Content = root };
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.UpdateLayout();
|
|
|
|
menu.IsPopupPinnedOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
menu.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>()
|
|
.IsPopupPinnedOpen.ShouldBeTrue();
|
|
|
|
// Inline 与 Vertical 之间往返会重建菜单项容器。
|
|
menu.Mode = NavMenuMode.Inline;
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.UpdateLayout();
|
|
menu.Mode = NavMenuMode.Vertical;
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.UpdateLayout();
|
|
|
|
var rebuilt = menu.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
|
|
rebuilt.IsPopupPinnedOpen.ShouldBeTrue(
|
|
"the pin request must be re-applied to containers rebuilt after the request was made");
|
|
rebuilt.IsSubMenuOpen.ShouldBeTrue();
|
|
rebuilt.Popup.ShouldNotBeNull().IsOpen.ShouldBeTrue();
|
|
|
|
// 钉住路径以节点为键,因此请求写入后追加的条目不会改变已解析的目标。
|
|
menu.Items.Add(new NavMenuNode { Header = "Late" });
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.UpdateLayout();
|
|
rebuilt.IsPopupPinnedOpen.ShouldBeTrue();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void IsPopupPinnedOpen_Is_Public_Api_For_Cross_Assembly_Previews()
|
|
{
|
|
// Gallery 的语义预览在另一个程序集里通过 AXAML 声明钉住打开,因此该属性必须公开。
|
|
// 先例:Tour / DropdownButton / AbstractSelect 等已经公开同一属性。
|
|
var property = typeof(AtomUINavMenu).GetProperty(nameof(AtomUINavMenu.IsPopupPinnedOpen));
|
|
property.ShouldNotBeNull();
|
|
property.GetGetMethod().ShouldNotBeNull().IsPublic.ShouldBeTrue();
|
|
property.GetSetMethod().ShouldNotBeNull().IsPublic.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a menu with one first-level leaf, one first-level group and one submenu that itself
|
|
/// contains a nested leaf and a group whose title and list markers are shared across levels.
|
|
/// </summary>
|
|
private static (AtomUINavMenu Menu, VisualLayerManager Root, NavMenuNode SubMenuNode) CreateMenuWithSubmenu(
|
|
NavMenuMode mode)
|
|
{
|
|
var nestedGroup = new NavMenuGroup { Header = "Nested Group" };
|
|
nestedGroup.Entries.Add(new NavMenuNode { Header = "Nested Group Leaf" });
|
|
|
|
var parent = new NavMenuNode { Header = "Parent" };
|
|
parent.Entries.Add(new NavMenuNode { Header = "Nested Leaf" });
|
|
parent.Entries.Add(nestedGroup);
|
|
|
|
var topGroup = new NavMenuGroup { Header = "Top Group" };
|
|
topGroup.Entries.Add(new NavMenuNode { Header = "Top Group Leaf" });
|
|
|
|
var menu = new AtomUINavMenu
|
|
{
|
|
Mode = mode,
|
|
IsMotionEnabled = false,
|
|
ShouldUseOverlayPopup = true
|
|
};
|
|
menu.Items.Add(new NavMenuNode { Header = "Leaf" });
|
|
menu.Items.Add(topGroup);
|
|
menu.Items.Add(parent);
|
|
|
|
var root = CreateRoot(menu);
|
|
return (menu, root, parent);
|
|
}
|
|
|
|
private static VisualLayerManager CreateRoot(Control content)
|
|
{
|
|
var visualLayerManager = new VisualLayerManager { Child = content };
|
|
var property = typeof(VisualLayerManager).GetProperty(
|
|
"EnablePopupOverlayLayer",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
property.ShouldNotBeNull();
|
|
property.SetValue(visualLayerManager, true);
|
|
return visualLayerManager;
|
|
}
|
|
|
|
private static void ShowInWindow(AtomUINavMenu menu, VisualLayerManager root, Action<VisualLayerManager> assertion)
|
|
{
|
|
var window = new AtomUIWindow
|
|
{
|
|
Width = 520,
|
|
Height = 400,
|
|
Content = root
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
if (menu.Mode != NavMenuMode.Inline)
|
|
{
|
|
menu.IsPopupPinnedOpen = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.UpdateLayout();
|
|
}
|
|
|
|
assertion(root);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
}
|
|
|
|
private static T[] Marked<T>(Visual root, string semanticClass) where T : Control
|
|
{
|
|
return root.GetVisualDescendants()
|
|
.OfType<T>()
|
|
.Where(control => control.Classes.Contains(semanticClass))
|
|
.ToArray();
|
|
}
|
|
|
|
private static Control[] Marked(Visual root, string semanticClass)
|
|
{
|
|
return root.GetVisualDescendants()
|
|
.OfType<Control>()
|
|
.Where(control => control.Classes.Contains(semanticClass))
|
|
.ToArray();
|
|
}
|
|
|
|
private static void AssertThemeMarkers(string relativePath, string[] expectedMarkers)
|
|
{
|
|
var document = XDocument.Load(GetRepoFile(relativePath), LoadOptions.SetLineInfo);
|
|
var literalSemanticMarkers = document.Descendants()
|
|
.Attributes()
|
|
.Where(static attribute =>
|
|
attribute.Name.LocalName == "Classes" &&
|
|
attribute.Value.Split(
|
|
(char[]?)null,
|
|
StringSplitOptions.RemoveEmptyEntries)
|
|
.Any(static value => value.StartsWith(
|
|
"semantic-",
|
|
StringComparison.Ordinal)))
|
|
.ToArray();
|
|
var classPropertyMarkers = document.Descendants()
|
|
.SelectMany(static element => element.Attributes()
|
|
.Where(static attribute => attribute.Name.LocalName.StartsWith(
|
|
"Classes.semantic-",
|
|
StringComparison.Ordinal))
|
|
.Select(attribute => (Element: element, Attribute: attribute)))
|
|
.ToArray();
|
|
|
|
literalSemanticMarkers.ShouldBeEmpty();
|
|
classPropertyMarkers.ShouldAllBe(static marker =>
|
|
string.Equals(marker.Attribute.Value, "true", StringComparison.OrdinalIgnoreCase));
|
|
classPropertyMarkers.Select(static marker =>
|
|
$"{marker.Attribute.Name.LocalName["Classes.".Length..]}:{marker.Element.Name.LocalName}")
|
|
.OrderBy(static value => value, StringComparer.Ordinal)
|
|
.ShouldBe(expectedMarkers);
|
|
}
|
|
|
|
private static string GetRepoFile(string relativePath)
|
|
{
|
|
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (directory is not null)
|
|
{
|
|
var candidate = Path.Combine(directory.FullName, relativePath);
|
|
if (File.Exists(candidate))
|
|
{
|
|
return candidate;
|
|
}
|
|
|
|
directory = directory.Parent;
|
|
}
|
|
|
|
throw new FileNotFoundException($"Could not locate repository file '{relativePath}'.");
|
|
}
|
|
}
|