mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-22 09:23:53 +08:00
476 lines
21 KiB
C#
476 lines
21 KiB
C#
using System.Xml.Linq;
|
|
using AtomUI.Theme;
|
|
using AtomUI.Theme.Schema;
|
|
using AtomUI.Theme.Styling;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Presenters;
|
|
using Avalonia.Styling;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AtomUICardTabControl = AtomUI.Desktop.Controls.CardTabControl;
|
|
using AtomUITabControl = AtomUI.Desktop.Controls.TabControl;
|
|
using AtomUITabItem = AtomUI.Desktop.Controls.TabItem;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.TabControl;
|
|
|
|
public class TabControlSemanticPartTests
|
|
{
|
|
private const string ItemClass = "semantic-item";
|
|
private const string HeaderClass = "semantic-header";
|
|
private const string ContentClass = "semantic-content";
|
|
private const string AddClass = "semantic-add";
|
|
private const string IndicatorClass = "semantic-indicator";
|
|
private const string IconClass = "semantic-icon";
|
|
private const string LabelClass = "semantic-label";
|
|
private const string CloseClass = "semantic-close";
|
|
|
|
static TabControlSemanticPartTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void Registered_Descriptors_Expose_The_Expected_Parts()
|
|
{
|
|
var registry = Application.Current.ShouldNotBeNull()
|
|
.GetThemeManager().ShouldNotBeNull()
|
|
.SemanticParts;
|
|
|
|
registry.TryGetControl(typeof(AtomUITabControl), out var tabControlDescriptor).ShouldBeTrue();
|
|
tabControlDescriptor.ShouldNotBeNull();
|
|
tabControlDescriptor.Parts.Select(static part => part.Name).ShouldBe(["root", "content", "header", "indicator", "item"]);
|
|
|
|
AssertRoot(tabControlDescriptor.Parts.Single(static part => part.Name == "root"), typeof(AtomUITabControl));
|
|
AssertPart(tabControlDescriptor.Parts.Single(static part => part.Name == "header"),
|
|
HeaderClass, "/template/ .semantic-header", typeof(Border),
|
|
SemanticPartCardinality.Single, typeof(TabControlHeaderStyle), runtimeCreated: false,
|
|
since: "6.2.0");
|
|
AssertPart(tabControlDescriptor.Parts.Single(static part => part.Name == "indicator"),
|
|
IndicatorClass, "/template/ .semantic-indicator", typeof(Border),
|
|
SemanticPartCardinality.Single, typeof(TabControlIndicatorStyle), runtimeCreated: false,
|
|
since: "6.2.0");
|
|
AssertPart(tabControlDescriptor.Parts.Single(static part => part.Name == "content"),
|
|
ContentClass, "/template/ .semantic-content", typeof(ContentPresenter),
|
|
SemanticPartCardinality.Single, typeof(TabControlContentStyle), runtimeCreated: false);
|
|
AssertPart(tabControlDescriptor.Parts.Single(static part => part.Name == "item"),
|
|
ItemClass, "> .semantic-item", typeof(AtomUITabItem),
|
|
SemanticPartCardinality.Multiple, typeof(TabControlItemStyle), runtimeCreated: true);
|
|
|
|
registry.TryGetControl(typeof(AtomUICardTabControl), out var cardDescriptor).ShouldBeTrue();
|
|
cardDescriptor.ShouldNotBeNull();
|
|
cardDescriptor.Parts.Select(static part => part.Name).ShouldBe(["root", "add", "content", "header", "item"]);
|
|
|
|
AssertRoot(cardDescriptor.Parts.Single(static part => part.Name == "root"), typeof(AtomUICardTabControl));
|
|
AssertPart(cardDescriptor.Parts.Single(static part => part.Name == "header"),
|
|
HeaderClass, "/template/ .semantic-header", typeof(Border),
|
|
SemanticPartCardinality.Single, typeof(CardTabControlHeaderStyle), runtimeCreated: false,
|
|
since: "6.2.0");
|
|
AssertPart(cardDescriptor.Parts.Single(static part => part.Name == "add"),
|
|
AddClass, "/template/ .semantic-add", typeof(IconButton),
|
|
SemanticPartCardinality.Single, typeof(CardTabControlAddStyle), runtimeCreated: false);
|
|
AssertPart(cardDescriptor.Parts.Single(static part => part.Name == "content"),
|
|
ContentClass, "/template/ .semantic-content", typeof(ContentPresenter),
|
|
SemanticPartCardinality.Single, typeof(CardTabControlContentStyle), runtimeCreated: false);
|
|
AssertPart(cardDescriptor.Parts.Single(static part => part.Name == "item"),
|
|
ItemClass, "> .semantic-item", typeof(AtomUITabItem),
|
|
SemanticPartCardinality.Multiple, typeof(CardTabControlItemStyle), runtimeCreated: true);
|
|
|
|
registry.TryGetControl(typeof(AtomUITabItem), out var tabItemDescriptor).ShouldBeTrue();
|
|
tabItemDescriptor.ShouldNotBeNull();
|
|
tabItemDescriptor.Parts.Select(static part => part.Name).ShouldBe(["root", "close", "icon", "label"]);
|
|
|
|
AssertRoot(tabItemDescriptor.Parts.Single(static part => part.Name == "root"), typeof(AtomUITabItem));
|
|
AssertPart(tabItemDescriptor.Parts.Single(static part => part.Name == "close"),
|
|
CloseClass, "/template/ .semantic-close", typeof(IconButton),
|
|
SemanticPartCardinality.Single, typeof(TabItemCloseStyle), runtimeCreated: false);
|
|
AssertPart(tabItemDescriptor.Parts.Single(static part => part.Name == "icon"),
|
|
IconClass, "/template/ .semantic-icon", typeof(AtomUI.Controls.IconPresenter),
|
|
SemanticPartCardinality.Single, typeof(TabItemIconStyle), runtimeCreated: false);
|
|
AssertPart(tabItemDescriptor.Parts.Single(static part => part.Name == "label"),
|
|
LabelClass, "/template/ .semantic-label", typeof(ContentPresenter),
|
|
SemanticPartCardinality.Single, typeof(TabItemLabelStyle), runtimeCreated: false);
|
|
|
|
registry.TryGetControl(typeof(BaseTabControl), out _).ShouldBeFalse();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("src/AtomUI.Desktop.Controls/TabControl/Themes/TabControlTheme.axaml",
|
|
new[] { "semantic-content:ContentPresenter", "semantic-header:Border", "semantic-indicator:Border" })]
|
|
[InlineData("src/AtomUI.Desktop.Controls/TabControl/Themes/CardTabControlTheme.axaml",
|
|
new[] { "semantic-add:IconButton", "semantic-content:ContentPresenter", "semantic-header:Border" })]
|
|
[InlineData("src/AtomUI.Desktop.Controls/TabControl/Themes/BaseTabItemTheme.axaml",
|
|
new[] { "semantic-close:IconButton", "semantic-icon:IconPresenter", "semantic-label:ContentPresenter" })]
|
|
[InlineData("src/AtomUI.Desktop.Controls/TabControl/Themes/CardTabItemTheme.axaml",
|
|
new[] { "semantic-close:IconButton", "semantic-icon:IconPresenter", "semantic-label:ContentPresenter" })]
|
|
public void Built_In_Themes_Declare_Expected_Static_Semantic_Markers(string relativePath, string[] expectedMarkers)
|
|
{
|
|
AssertThemeMarkers(relativePath, expectedMarkers);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("src/AtomUI.Desktop.Controls/TabControl/Themes/BaseTabControlTheme.axaml")]
|
|
[InlineData("src/AtomUI.Desktop.Controls/TabControl/Themes/TabItemTheme.axaml")]
|
|
public void Built_In_Themes_Declare_No_Static_Semantic_Markers(string relativePath)
|
|
{
|
|
AssertThemeMarkers(relativePath, Array.Empty<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public void Realized_Tab_Items_Are_Item_Parts()
|
|
{
|
|
var tabControl = new AtomUITabControl();
|
|
tabControl.ItemsSource = new[] { "First", "Second", "Third" };
|
|
|
|
using var window = Show(tabControl);
|
|
|
|
var items = GetSemanticElements(tabControl, ItemClass).OfType<AtomUITabItem>().ToArray();
|
|
items.Length.ShouldBe(3);
|
|
tabControl.Classes.ShouldNotContain("semantic-root");
|
|
}
|
|
|
|
[Fact]
|
|
public void User_Provided_TabItem_Instances_Receive_The_Item_Marker()
|
|
{
|
|
var tabControl = new AtomUITabControl();
|
|
tabControl.Items.Add(new AtomUITabItem { Header = "A" });
|
|
tabControl.Items.Add(new AtomUITabItem { Header = "B" });
|
|
|
|
using var window = Show(tabControl);
|
|
|
|
GetSemanticElements(tabControl, ItemClass).OfType<AtomUITabItem>().ToArray().Length.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void Generated_Item_And_Content_Styles_Apply_To_Expected_Elements()
|
|
{
|
|
var tabControl = new AtomUITabControl();
|
|
tabControl.ItemsSource = new[] { "First", "Second" };
|
|
tabControl.Classes.Add("semantic-owner");
|
|
|
|
var ownerStyle = new Style(selector => selector.OfType<AtomUITabControl>().Class("semantic-owner"));
|
|
ownerStyle.Children.Add(new TabControlItemStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "item") }
|
|
});
|
|
ownerStyle.Children.Add(new TabControlContentStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "content") }
|
|
});
|
|
tabControl.Styles.Add(ownerStyle);
|
|
|
|
using var window = Show(tabControl);
|
|
|
|
GetSemanticElements(tabControl, ItemClass).OfType<Control>()
|
|
.ShouldAllBe(static control => Equals(control.Tag, "item"));
|
|
GetSemanticElements(tabControl, ContentClass).OfType<Control>()
|
|
.ShouldAllBe(static control => Equals(control.Tag, "content"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Generated_Add_Style_Applies_To_The_Add_Button_Only()
|
|
{
|
|
var cardTabControl = new AtomUICardTabControl();
|
|
cardTabControl.ItemsSource = new[] { "First" };
|
|
cardTabControl.Classes.Add("semantic-owner");
|
|
|
|
var ownerStyle = new Style(selector => selector.OfType<AtomUICardTabControl>().Class("semantic-owner"));
|
|
ownerStyle.Children.Add(new CardTabControlAddStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "add") }
|
|
});
|
|
cardTabControl.Styles.Add(ownerStyle);
|
|
|
|
using var window = Show(cardTabControl);
|
|
|
|
var addButtons = GetSemanticElements(cardTabControl, AddClass).OfType<Control>().ToArray();
|
|
addButtons.Length.ShouldBe(1);
|
|
addButtons[0].Tag.ShouldBe("add");
|
|
GetSemanticElements(cardTabControl, ItemClass).OfType<Control>()
|
|
.ShouldAllBe(static control => control.Tag == null);
|
|
}
|
|
|
|
[Fact]
|
|
public void Tab_Items_Carry_Icon_Label_And_Close_Markers_In_Line_And_Card_Templates()
|
|
{
|
|
var tabControl = new AtomUITabControl();
|
|
tabControl.ItemsSource = new[] { "A" };
|
|
|
|
using (var window = Show(tabControl))
|
|
{
|
|
AssertItemSubPartMarkers(tabControl);
|
|
}
|
|
|
|
var cardTabControl = new AtomUICardTabControl();
|
|
cardTabControl.ItemsSource = new[] { "A" };
|
|
|
|
using (var window = Show(cardTabControl))
|
|
{
|
|
AssertItemSubPartMarkers(cardTabControl);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Generated_TabItem_SubPart_Styles_Apply_Within_Owner_Scope()
|
|
{
|
|
var tabControl = new AtomUITabControl();
|
|
var styledItem = new AtomUITabItem { Header = "A" };
|
|
styledItem.Classes.Add("semantic-owner");
|
|
|
|
var ownerStyle = new Style(selector => selector.OfType<AtomUITabItem>().Class("semantic-owner"));
|
|
ownerStyle.Children.Add(new TabItemIconStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "icon") }
|
|
});
|
|
ownerStyle.Children.Add(new TabItemLabelStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "label") }
|
|
});
|
|
ownerStyle.Children.Add(new TabItemCloseStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "close") }
|
|
});
|
|
styledItem.Styles.Add(ownerStyle);
|
|
|
|
tabControl.Items.Add(styledItem);
|
|
tabControl.Items.Add(new AtomUITabItem { Header = "B" });
|
|
|
|
using var window = Show(tabControl);
|
|
|
|
styledItem.GetVisualDescendants().OfType<Control>().Single(
|
|
static control => control.Classes.Contains(IconClass)).Tag.ShouldBe("icon");
|
|
styledItem.GetVisualDescendants().OfType<Control>().Single(
|
|
static control => control.Classes.Contains(LabelClass)).Tag.ShouldBe("label");
|
|
styledItem.GetVisualDescendants().OfType<Control>().Single(
|
|
static control => control.Classes.Contains(CloseClass)).Tag.ShouldBe("close");
|
|
|
|
var unstyledItem = tabControl.GetVisualDescendants().OfType<AtomUITabItem>()
|
|
.Single(item => item != styledItem);
|
|
unstyledItem.GetVisualDescendants().OfType<Control>().ShouldAllBe(static control =>
|
|
!Equals(control.Tag, "icon") && !Equals(control.Tag, "label") && !Equals(control.Tag, "close"));
|
|
}
|
|
|
|
[Fact]
|
|
public void SizeType_Baseline_Is_Preserved_When_Semantic_Styles_Override_Item_Padding()
|
|
{
|
|
AtomUITabControl CreateControl(AtomUI.SizeType sizeType)
|
|
{
|
|
var tabControl = new AtomUITabControl
|
|
{
|
|
SizeType = sizeType
|
|
};
|
|
tabControl.ItemsSource = new[] { "First" };
|
|
tabControl.Classes.Add("semantic-owner");
|
|
|
|
var ownerStyle = new Style(selector => selector.OfType<AtomUITabControl>().Class("semantic-owner"));
|
|
ownerStyle.Children.Add(new TabControlItemStyle
|
|
{
|
|
Setters = { new Setter(AtomUITabItem.PaddingProperty, new Thickness(0)) }
|
|
});
|
|
tabControl.Styles.Add(ownerStyle);
|
|
return tabControl;
|
|
}
|
|
|
|
var large = CreateControl(AtomUI.SizeType.Large);
|
|
var small = CreateControl(AtomUI.SizeType.Small);
|
|
|
|
using var window = Show(new StackPanel
|
|
{
|
|
Children = { large, small }
|
|
});
|
|
|
|
var largeItem = large.GetVisualDescendants().OfType<AtomUITabItem>().Single();
|
|
var smallItem = small.GetVisualDescendants().OfType<AtomUITabItem>().Single();
|
|
|
|
largeItem.Padding.ShouldBe(new Thickness(0));
|
|
smallItem.Padding.ShouldBe(new Thickness(0));
|
|
largeItem.FontSize.ShouldBeGreaterThan(smallItem.FontSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void Generated_Header_Styles_Apply_To_The_Header_Container()
|
|
{
|
|
var tabControl = new AtomUITabControl();
|
|
tabControl.ItemsSource = new[] { "First" };
|
|
tabControl.Classes.Add("semantic-owner");
|
|
|
|
var ownerStyle = new Style(selector => selector.OfType<AtomUITabControl>().Class("semantic-owner"));
|
|
ownerStyle.Children.Add(new TabControlHeaderStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "header") }
|
|
});
|
|
tabControl.Styles.Add(ownerStyle);
|
|
|
|
using (Show(tabControl))
|
|
{
|
|
GetSemanticElements(tabControl, HeaderClass).OfType<Control>()
|
|
.Single().Tag.ShouldBe("header");
|
|
}
|
|
|
|
var cardTabControl = new AtomUICardTabControl();
|
|
cardTabControl.ItemsSource = new[] { "First" };
|
|
cardTabControl.Classes.Add("semantic-owner");
|
|
|
|
var cardOwnerStyle = new Style(selector => selector.OfType<AtomUICardTabControl>().Class("semantic-owner"));
|
|
cardOwnerStyle.Children.Add(new CardTabControlHeaderStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "header") }
|
|
});
|
|
cardTabControl.Styles.Add(cardOwnerStyle);
|
|
|
|
using (Show(cardTabControl))
|
|
{
|
|
GetSemanticElements(cardTabControl, HeaderClass).OfType<Control>()
|
|
.Single().Tag.ShouldBe("header");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Generated_Indicator_Style_Applies_To_The_Indicator_Border()
|
|
{
|
|
var tabControl = new AtomUITabControl();
|
|
tabControl.ItemsSource = new[] { "First" };
|
|
tabControl.Classes.Add("semantic-owner");
|
|
|
|
var ownerStyle = new Style(selector => selector.OfType<AtomUITabControl>().Class("semantic-owner"));
|
|
ownerStyle.Children.Add(new TabControlIndicatorStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "indicator") }
|
|
});
|
|
tabControl.Styles.Add(ownerStyle);
|
|
|
|
using (Show(tabControl))
|
|
{
|
|
GetSemanticElements(tabControl, IndicatorClass).OfType<Control>()
|
|
.Single().Tag.ShouldBe("indicator");
|
|
}
|
|
}
|
|
|
|
private static void AssertRoot(SemanticPartDescriptor part, Type ownerType)
|
|
{
|
|
part.Path.ShouldBe("root");
|
|
part.SelectorClass.ShouldBeNull();
|
|
part.SelectorRoute.ShouldBeNull();
|
|
part.ContractType.ShouldBe(ownerType);
|
|
part.Cardinality.ShouldBe(SemanticPartCardinality.Single);
|
|
part.Customization.ShouldBe(SemanticPartCustomization.Root);
|
|
part.CrossVisualRoot.ShouldBeFalse();
|
|
part.RuntimeCreated.ShouldBeFalse();
|
|
part.StyleType.ShouldBeNull();
|
|
part.Since.ShouldBe("6.2.0");
|
|
}
|
|
|
|
private static void AssertPart(
|
|
SemanticPartDescriptor part,
|
|
string selectorClass,
|
|
string selectorRoute,
|
|
Type contractType,
|
|
SemanticPartCardinality cardinality,
|
|
Type styleType,
|
|
bool runtimeCreated,
|
|
string since = "6.2.0")
|
|
{
|
|
part.SelectorClass.ShouldBe(selectorClass);
|
|
part.SelectorRoute.ShouldBe(selectorRoute);
|
|
part.ContractType.ShouldBe(contractType);
|
|
part.Cardinality.ShouldBe(cardinality);
|
|
part.Customization.ShouldBe(SemanticPartCustomization.Selector);
|
|
part.CrossVisualRoot.ShouldBeFalse();
|
|
part.RuntimeCreated.ShouldBe(runtimeCreated);
|
|
part.StyleType.ShouldBe(styleType);
|
|
part.Since.ShouldBe(since);
|
|
}
|
|
|
|
private static void AssertItemSubPartMarkers(Control owner)
|
|
{
|
|
var item = owner.GetVisualDescendants().OfType<AtomUITabItem>().Single();
|
|
item.GetVisualDescendants().OfType<Control>().Count(
|
|
static control => control.Classes.Contains(IconClass)).ShouldBe(1);
|
|
item.GetVisualDescendants().OfType<Control>().Count(
|
|
static control => control.Classes.Contains(LabelClass)).ShouldBe(1);
|
|
item.GetVisualDescendants().OfType<Control>().Count(
|
|
static control => control.Classes.Contains(CloseClass)).ShouldBe(1);
|
|
}
|
|
|
|
private static Control[] GetSemanticElements(Control owner, string semanticClass)
|
|
{
|
|
return owner.GetVisualDescendants()
|
|
.OfType<Control>()
|
|
.Where(control => control.Classes.Contains(semanticClass))
|
|
.ToArray();
|
|
}
|
|
|
|
private static IDisposable Show(Control content)
|
|
{
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 640,
|
|
Height = 260,
|
|
Content = content
|
|
};
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
return new WindowLifetime(window);
|
|
}
|
|
|
|
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}'.");
|
|
}
|
|
|
|
private sealed class WindowLifetime(AvaloniaWindow window) : IDisposable
|
|
{
|
|
public void Dispose()
|
|
{
|
|
window.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
}
|
|
}
|