mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-29 07:16:33 +08:00
402 lines
16 KiB
C#
402 lines
16 KiB
C#
using System.Xml.Linq;
|
|
using AtomUI.Controls.Primitives;
|
|
using AtomUI.Theme;
|
|
using AtomUI.Theme.Schema;
|
|
using AtomUI.Theme.Styling;
|
|
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 AtomUISteps = AtomUI.Desktop.Controls.Steps;
|
|
using AtomUIStepsItem = AtomUI.Desktop.Controls.StepsItem;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.Steps;
|
|
|
|
public class StepsSemanticPartTests
|
|
{
|
|
private const string ItemClass = "semantic-item";
|
|
private const string ItemWrapperClass = "semantic-item-wrapper";
|
|
private const string ItemIconClass = "semantic-item-icon";
|
|
private const string ItemTitleClass = "semantic-item-title";
|
|
private const string ItemSubtitleClass = "semantic-item-subtitle";
|
|
private const string ItemSectionClass = "semantic-item-section";
|
|
private const string ItemContentClass = "semantic-item-content";
|
|
private const string ItemRailClass = "semantic-item-rail";
|
|
|
|
static StepsSemanticPartTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void Registered_Descriptors_Expose_The_Ant_Design_Aligned_Parts()
|
|
{
|
|
var registry = Application.Current.ShouldNotBeNull()
|
|
.GetThemeManager().ShouldNotBeNull()
|
|
.SemanticParts;
|
|
|
|
registry.TryGetControl(typeof(AtomUISteps), out var stepsDescriptor).ShouldBeTrue();
|
|
stepsDescriptor.ShouldNotBeNull();
|
|
stepsDescriptor.Parts.Select(static part => part.Name).ShouldBe(
|
|
[
|
|
"root",
|
|
"item",
|
|
"itemContent",
|
|
"itemIcon",
|
|
"itemRail",
|
|
"itemSection",
|
|
"itemSubtitle",
|
|
"itemTitle",
|
|
"itemWrapper"
|
|
]);
|
|
|
|
AssertRoot(stepsDescriptor.Parts.Single(static part => part.Name == "root"), typeof(AtomUISteps));
|
|
|
|
AssertItemPart(stepsDescriptor.Parts.Single(static part => part.Name == "item"),
|
|
"semantic-item",
|
|
"> .semantic-item",
|
|
typeof(AtomUIStepsItem),
|
|
typeof(StepsItemStyle));
|
|
AssertItemPart(stepsDescriptor.Parts.Single(static part => part.Name == "itemWrapper"),
|
|
ItemWrapperClass,
|
|
"> .semantic-item /template/ .semantic-item-wrapper",
|
|
typeof(Border),
|
|
typeof(StepsItemWrapperStyle));
|
|
AssertItemPart(stepsDescriptor.Parts.Single(static part => part.Name == "itemIcon"),
|
|
ItemIconClass,
|
|
"> .semantic-item /template/ .semantic-item-icon",
|
|
typeof(TemplatedControl),
|
|
typeof(StepsItemIconStyle));
|
|
AssertItemPart(stepsDescriptor.Parts.Single(static part => part.Name == "itemTitle"),
|
|
ItemTitleClass,
|
|
"> .semantic-item /template/ .semantic-item-title",
|
|
typeof(ContentPresenter),
|
|
typeof(StepsItemTitleStyle));
|
|
AssertItemPart(stepsDescriptor.Parts.Single(static part => part.Name == "itemSubtitle"),
|
|
ItemSubtitleClass,
|
|
"> .semantic-item /template/ .semantic-item-subtitle",
|
|
typeof(ContentPresenter),
|
|
typeof(StepsItemSubtitleStyle));
|
|
AssertItemPart(stepsDescriptor.Parts.Single(static part => part.Name == "itemSection"),
|
|
ItemSectionClass,
|
|
"> .semantic-item /template/ .semantic-item-section",
|
|
typeof(Panel),
|
|
typeof(StepsItemSectionStyle));
|
|
AssertItemPart(stepsDescriptor.Parts.Single(static part => part.Name == "itemContent"),
|
|
ItemContentClass,
|
|
"> .semantic-item /template/ .semantic-item-content",
|
|
typeof(ContentPresenter),
|
|
typeof(StepsItemContentStyle));
|
|
AssertItemPart(stepsDescriptor.Parts.Single(static part => part.Name == "itemRail"),
|
|
ItemRailClass,
|
|
"> .semantic-item /template/ .semantic-item-rail",
|
|
typeof(DashedBorder),
|
|
typeof(StepsItemRailStyle));
|
|
|
|
registry.TryGetControl(typeof(AtomUIStepsItem), out _).ShouldBeFalse();
|
|
registry.TryGetControl(typeof(StepsPanel), out _).ShouldBeFalse();
|
|
registry.TryGetControl(typeof(StepsItemIndicator), out _).ShouldBeFalse();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("src/AtomUI.Desktop.Controls/Steps/Themes/StepsItemTheme.axaml",
|
|
new[]
|
|
{
|
|
"semantic-item-content:ContentPresenter",
|
|
"semantic-item-icon:StepsItemIndicator",
|
|
"semantic-item-rail:PixelAlignedBorder",
|
|
"semantic-item-section:StepsItemSectionPanel",
|
|
"semantic-item-subtitle:ContentPresenter",
|
|
"semantic-item-title:ContentPresenter",
|
|
"semantic-item-wrapper:StepsPanelItemFrame"
|
|
})]
|
|
public void Built_In_Themes_Declare_Expected_Static_Semantic_Markers(string relativePath, string[] expectedMarkers)
|
|
{
|
|
AssertThemeMarkers(relativePath, expectedMarkers);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("src/AtomUI.Desktop.Controls/Steps/Themes/StepsTheme.axaml")]
|
|
[InlineData("src/AtomUI.Desktop.Controls/Steps/Themes/StepsItemIndicatorTheme.axaml")]
|
|
public void Built_In_Themes_Declare_No_Static_Semantic_Markers(string relativePath)
|
|
{
|
|
AssertThemeMarkers(relativePath, Array.Empty<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public void Realized_Items_Are_Item_Parts_With_All_Sub_Part_Markers()
|
|
{
|
|
var steps = new AtomUISteps
|
|
{
|
|
Current = 1
|
|
};
|
|
steps.Items.Add(new AtomUIStepsItem
|
|
{
|
|
Header = "First",
|
|
SubHeader = "First description",
|
|
Content = "First content"
|
|
});
|
|
steps.Items.Add(new AtomUIStepsItem
|
|
{
|
|
Header = "Second",
|
|
SubHeader = "Second description",
|
|
Content = "Second content"
|
|
});
|
|
|
|
using var window = Show(steps);
|
|
|
|
var items = GetSemanticElements(steps, ItemClass).OfType<AtomUIStepsItem>().ToArray();
|
|
items.Length.ShouldBe(2);
|
|
steps.Classes.ShouldNotContain("semantic-root");
|
|
|
|
foreach (var item in items)
|
|
{
|
|
GetSemanticElements(item, ItemWrapperClass).Length.ShouldBe(1);
|
|
GetSemanticElements(item, ItemIconClass).Length.ShouldBe(1);
|
|
GetSemanticElements(item, ItemTitleClass).Length.ShouldBe(1);
|
|
GetSemanticElements(item, ItemSubtitleClass).Length.ShouldBe(1);
|
|
GetSemanticElements(item, ItemSectionClass).Length.ShouldBe(1);
|
|
GetSemanticElements(item, ItemContentClass).Length.ShouldBe(1);
|
|
GetSemanticElements(item, ItemRailClass).Length.ShouldBe(1);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Generated_Item_Part_Styles_Apply_To_The_Expected_Template_Nodes()
|
|
{
|
|
var steps = new AtomUISteps
|
|
{
|
|
Current = 1
|
|
};
|
|
steps.Items.Add(new AtomUIStepsItem
|
|
{
|
|
Header = "First",
|
|
SubHeader = "First description",
|
|
Content = "First content"
|
|
});
|
|
steps.Items.Add(new AtomUIStepsItem
|
|
{
|
|
Header = "Second",
|
|
SubHeader = "Second description",
|
|
Content = "Second content"
|
|
});
|
|
steps.Classes.Add("semantic-owner");
|
|
|
|
var ownerStyle = new Style(selector => selector.OfType<AtomUISteps>().Class("semantic-owner"));
|
|
ownerStyle.Children.Add(new StepsItemStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "item") }
|
|
});
|
|
ownerStyle.Children.Add(new StepsItemWrapperStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "wrapper") }
|
|
});
|
|
ownerStyle.Children.Add(new StepsItemIconStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "icon") }
|
|
});
|
|
ownerStyle.Children.Add(new StepsItemTitleStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "title") }
|
|
});
|
|
ownerStyle.Children.Add(new StepsItemSubtitleStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "subtitle") }
|
|
});
|
|
ownerStyle.Children.Add(new StepsItemSectionStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "section") }
|
|
});
|
|
ownerStyle.Children.Add(new StepsItemContentStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "content") }
|
|
});
|
|
ownerStyle.Children.Add(new StepsItemRailStyle
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "rail") }
|
|
});
|
|
steps.Styles.Add(ownerStyle);
|
|
|
|
using var window = Show(steps);
|
|
|
|
var items = GetSemanticElements(steps, ItemClass).OfType<AtomUIStepsItem>().ToArray();
|
|
items.Length.ShouldBe(2);
|
|
items.ShouldAllBe(static item => Equals(item.Tag, "item"));
|
|
|
|
foreach (var item in items)
|
|
{
|
|
GetSemanticElements(item, ItemWrapperClass).OfType<Control>().Single().Tag.ShouldBe("wrapper");
|
|
GetSemanticElements(item, ItemIconClass).OfType<Control>().Single().Tag.ShouldBe("icon");
|
|
GetSemanticElements(item, ItemTitleClass).OfType<Control>().Single().Tag.ShouldBe("title");
|
|
GetSemanticElements(item, ItemSubtitleClass).OfType<Control>().Single().Tag.ShouldBe("subtitle");
|
|
GetSemanticElements(item, ItemSectionClass).OfType<Control>().Single().Tag.ShouldBe("section");
|
|
GetSemanticElements(item, ItemContentClass).OfType<Control>().Single().Tag.ShouldBe("content");
|
|
GetSemanticElements(item, ItemRailClass).OfType<Control>().Single().Tag.ShouldBe("rail");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Item_Icon_Corner_Radius_Defaults_To_The_Circular_Frame()
|
|
{
|
|
var steps = new AtomUISteps
|
|
{
|
|
Current = 1
|
|
};
|
|
steps.Items.Add(new AtomUIStepsItem
|
|
{
|
|
Header = "First",
|
|
SubHeader = "First description",
|
|
Content = "First content"
|
|
});
|
|
|
|
using var window = Show(steps);
|
|
|
|
var icon = GetSemanticElements(steps, ItemIconClass).OfType<TemplatedControl>().Single();
|
|
icon.CornerRadius.ShouldBe(new CornerRadius(32));
|
|
}
|
|
|
|
[Fact]
|
|
public void Generated_ItemIcon_Style_Overrides_The_Default_Circular_Corner_Radius()
|
|
{
|
|
var steps = new AtomUISteps
|
|
{
|
|
Current = 1
|
|
};
|
|
steps.Items.Add(new AtomUIStepsItem
|
|
{
|
|
Header = "First",
|
|
SubHeader = "First description",
|
|
Content = "First content"
|
|
});
|
|
steps.Classes.Add("semantic-owner");
|
|
|
|
var ownerStyle = new Style(selector => selector.OfType<AtomUISteps>().Class("semantic-owner"));
|
|
ownerStyle.Children.Add(new StepsItemIconStyle
|
|
{
|
|
Setters = { new Setter(TemplatedControl.CornerRadiusProperty, new CornerRadius(10)) }
|
|
});
|
|
steps.Styles.Add(ownerStyle);
|
|
|
|
using var window = Show(steps);
|
|
|
|
var icon = GetSemanticElements(steps, ItemIconClass).OfType<TemplatedControl>().Single();
|
|
icon.CornerRadius.ShouldBe(new CornerRadius(10));
|
|
}
|
|
|
|
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 AssertItemPart(
|
|
SemanticPartDescriptor part,
|
|
string selectorClass,
|
|
string selectorRoute,
|
|
Type contractType,
|
|
Type styleType)
|
|
{
|
|
part.SelectorClass.ShouldBe(selectorClass);
|
|
part.SelectorRoute.ShouldBe(selectorRoute);
|
|
part.ContractType.ShouldBe(contractType);
|
|
part.Cardinality.ShouldBe(SemanticPartCardinality.Multiple);
|
|
part.Customization.ShouldBe(SemanticPartCustomization.Selector);
|
|
part.CrossVisualRoot.ShouldBeFalse();
|
|
part.RuntimeCreated.ShouldBeTrue();
|
|
part.StyleType.ShouldBe(styleType);
|
|
part.Since.ShouldBe("6.2.0");
|
|
}
|
|
|
|
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 = 900,
|
|
Height = 400,
|
|
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();
|
|
}
|
|
}
|
|
}
|