mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-28 14:20:47 +08:00
476 lines
15 KiB
C#
476 lines
15 KiB
C#
using AtomUI.Theme.Styling;
|
|
using System.Collections.ObjectModel;
|
|
using AtomUI.Controls.Data;
|
|
using AtomUI.Controls.Primitives;
|
|
using AtomUI.Desktop.Controls.Primitives;
|
|
using AtomUI.Theme;
|
|
using AtomUI.Theme.Schema;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Media;
|
|
using Avalonia.Styling;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AtomListBox = AtomUI.Desktop.Controls.ListBox;
|
|
using AtomListBoxItem = AtomUI.Desktop.Controls.ListBoxItem;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.ListBox;
|
|
|
|
public class ListBoxSemanticPartTests
|
|
{
|
|
private const string ItemClass = "semantic-item";
|
|
private const string ItemRoute = "> .semantic-item";
|
|
|
|
static ListBoxSemanticPartTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void Registered_Descriptor_Exposes_Only_The_Approved_Parts()
|
|
{
|
|
var registry = Application.Current.ShouldNotBeNull()
|
|
.GetThemeManager().ShouldNotBeNull()
|
|
.SemanticParts;
|
|
|
|
registry.TryGetControl(typeof(AtomListBox), out var descriptor).ShouldBeTrue();
|
|
descriptor.ShouldNotBeNull();
|
|
descriptor.Parts.Select(static part => part.Name)
|
|
.ShouldBe(["root", "item"]);
|
|
|
|
AssertRoot(descriptor.Parts.Single(static part => part.Name == "root"));
|
|
AssertPart(descriptor.Parts.Single(static part => part.Name == "item"),
|
|
ItemClass,
|
|
typeof(AtomListBoxItem),
|
|
ItemRoute);
|
|
}
|
|
|
|
[Fact]
|
|
public void Items_Produce_One_Item_Marker_Per_Container()
|
|
{
|
|
var listBox = new AtomListBox
|
|
{
|
|
ItemsSource = CreateItems("Olivia", "Liam", "Emma")
|
|
};
|
|
|
|
ShowInWindow(listBox, () =>
|
|
{
|
|
GetSemanticItems(listBox).Length.ShouldBe(3);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Explicit_Item_Container_Receives_The_Semantic_Marker()
|
|
{
|
|
var view = new SemanticParts.SemanticRepairStylesView();
|
|
var listBox = view.FindControl<AtomListBox>("ListBox").ShouldNotBeNull();
|
|
var style = view.Styles.OfType<Style>().SelectMany(owner => owner.Children)
|
|
.OfType<ListBoxItemStyle>().ShouldHaveSingleItem();
|
|
style.Setters.OfType<Setter>().Single(setter => setter.Property == Control.TagProperty)
|
|
.Value.ShouldBe("item");
|
|
style.Setters.OfType<Setter>().Single(setter => setter.Property == TemplatedControl.BackgroundProperty)
|
|
.Value.ShouldBe(Brushes.Red);
|
|
ShowInWindow(view, () =>
|
|
{
|
|
var item = GetContainers(listBox).Single();
|
|
item.Classes.Contains(ItemClass).ShouldBeTrue();
|
|
item.Tag.ShouldBe("item");
|
|
item.Background.ShouldBe(Brushes.Red);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Divider_Uses_Container_Position_Not_Item_Reference()
|
|
{
|
|
object repeated = new();
|
|
var listBox = new AtomListBox
|
|
{
|
|
ItemsSource = new object[] { repeated, new object(), repeated }
|
|
};
|
|
|
|
ShowInWindow(listBox, () =>
|
|
{
|
|
GetSemanticItems(listBox).Select(GetSplitLineFrame)
|
|
.Select(static frame => frame.IsVisible)
|
|
.ShouldBe([true, true, false]);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Candidate_List_Containers_Do_Not_Carry_The_ListBox_Item_Marker()
|
|
{
|
|
var candidateList = new CandidateList
|
|
{
|
|
ItemsSource = CreateItems("Olivia", "Liam")
|
|
};
|
|
|
|
ShowInWindow(candidateList, () =>
|
|
{
|
|
GetContainers(candidateList).Length.ShouldBe(2);
|
|
GetContainers(candidateList).ShouldAllBe(static item => !item.Classes.Contains(ItemClass));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Root_Semantic_Part_Projects_The_Standard_Surface_Properties()
|
|
{
|
|
var listBox = new AtomListBox
|
|
{
|
|
ItemsSource = CreateItems("Olivia")
|
|
};
|
|
listBox.Background = new SolidColorBrush(Colors.AliceBlue);
|
|
listBox.BorderBrush = new SolidColorBrush(Color.Parse("#CDC1FF"));
|
|
listBox.BorderThickness = new Thickness(1);
|
|
listBox.CornerRadius = new CornerRadius(8);
|
|
listBox.Padding = new Thickness(10);
|
|
|
|
ShowInWindow(listBox, () =>
|
|
{
|
|
var frame = GetFrame(listBox);
|
|
|
|
frame.Background.ShouldBeSameAs(listBox.Background);
|
|
frame.BorderBrush.ShouldBeSameAs(listBox.BorderBrush);
|
|
frame.BorderThickness.ShouldBe(new Thickness(1));
|
|
frame.CornerRadius.ShouldBe(new CornerRadius(8));
|
|
frame.Padding.ShouldBe(new Thickness(10));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Semantic_Selectors_Match_Their_Declared_Owner_Routes()
|
|
{
|
|
var listBox = new AtomListBox
|
|
{
|
|
ItemsSource = CreateItems("Olivia", "Liam")
|
|
};
|
|
listBox.Classes.Add("semantic-owner");
|
|
listBox.Styles.Add(CreateRouteStyle());
|
|
|
|
ShowInWindow(listBox, () =>
|
|
{
|
|
GetSemanticItems(listBox).ShouldAllBe(static item => Equals(item.Tag, "item"));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Item_Containers_Get_A_Bottom_Divider()
|
|
{
|
|
var listBox = new AtomListBox
|
|
{
|
|
ItemsSource = CreateItems("Olivia", "Liam", "Emma")
|
|
};
|
|
|
|
ShowInWindow(listBox, () =>
|
|
{
|
|
var splitLines = GetSemanticItems(listBox).Select(GetSplitLineFrame).ToArray();
|
|
splitLines[0].IsVisible.ShouldBeTrue();
|
|
splitLines[0].BorderThickness.Bottom.ShouldBe(1);
|
|
splitLines[1].IsVisible.ShouldBeTrue();
|
|
splitLines[2].IsVisible.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Semantic_Item_Border_Style_Overrides_The_Default_Divider()
|
|
{
|
|
var listBox = new AtomListBox
|
|
{
|
|
ItemsSource = CreateItems("Olivia", "Liam")
|
|
};
|
|
listBox.Classes.Add("semantic-owner");
|
|
listBox.Styles.Add(new Style(selector =>
|
|
selector.OfType<AtomListBox>()
|
|
.Class("semantic-owner")
|
|
.Child()
|
|
.Class(ItemClass))
|
|
{
|
|
Setters = { new Setter(TemplatedControl.BorderThicknessProperty, new Thickness(2)) }
|
|
});
|
|
|
|
ShowInWindow(listBox, () =>
|
|
{
|
|
var items = GetSemanticItems(listBox);
|
|
var styledSplitLine = GetSplitLineFrame(items[0]);
|
|
styledSplitLine.IsVisible.ShouldBeTrue();
|
|
styledSplitLine.BorderThickness.ShouldBe(new Thickness(2));
|
|
GetSplitLineFrame(items[1]).IsVisible.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Borderless_Keeps_The_Divider_On_The_Last_Item()
|
|
{
|
|
var listBox = new AtomListBox
|
|
{
|
|
IsBorderless = true,
|
|
ItemsSource = CreateItems("Olivia", "Liam")
|
|
};
|
|
|
|
ShowInWindow(listBox, () =>
|
|
{
|
|
GetSemanticItems(listBox).Select(GetSplitLineFrame)
|
|
.ShouldAllBe(static line => line.IsVisible);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Appending_An_Item_Restores_The_Previously_Last_Items_Divider()
|
|
{
|
|
var items = new ObservableCollection<ListItemData>(CreateItems("Olivia", "Liam"));
|
|
var listBox = new AtomListBox
|
|
{
|
|
ItemsSource = items
|
|
};
|
|
|
|
ShowInWindow(listBox, () =>
|
|
{
|
|
GetSplitLineFrame(GetSemanticItems(listBox)[1]).IsVisible.ShouldBeFalse();
|
|
|
|
items.Add(new ListItemData { Content = "Emma" });
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var splitLines = GetSemanticItems(listBox).Select(GetSplitLineFrame).ToArray();
|
|
splitLines[0].IsVisible.ShouldBeTrue();
|
|
splitLines[1].IsVisible.ShouldBeTrue();
|
|
splitLines[2].IsVisible.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Root_Frame_Declares_Content_Clipping_To_The_Inner_Border_Edge()
|
|
{
|
|
var listBox = new AtomListBox
|
|
{
|
|
ItemsSource = CreateItems("Olivia")
|
|
};
|
|
|
|
ShowInWindow(listBox, () =>
|
|
{
|
|
var frame = GetFrame(listBox);
|
|
frame.ClipContentToCornerRadius.ShouldBeTrue();
|
|
// The clip figure is computed from the frame's own border thickness and
|
|
// corner radius, so the inner edge of the ring is the clipping boundary.
|
|
frame.Child.ShouldNotBeNull();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Candidate_And_Cascader_Items_Do_Not_Inherit_The_List_Divider()
|
|
{
|
|
var candidateList = new CandidateList { ItemsSource = new[] { "Time", "Date" } };
|
|
var cascaderList = new CascaderViewFilterList
|
|
{
|
|
ItemsSource = new CascaderViewFilterListItemData[]
|
|
{
|
|
new() { Content = "Asia / Shanghai" },
|
|
new() { Content = "Europe / London" }
|
|
}
|
|
};
|
|
|
|
ShowInWindow(new StackPanel { Children = { candidateList, cascaderList } }, () =>
|
|
{
|
|
var items = candidateList.GetVisualDescendants().OfType<CandidateListItem>().Cast<AtomListBoxItem>()
|
|
.Concat(cascaderList.GetVisualDescendants().OfType<CascaderViewFilterListItem>())
|
|
.ToArray();
|
|
items.Length.ShouldBe(4);
|
|
foreach (var item in items)
|
|
{
|
|
var splitLine = GetSplitLineFrame(item);
|
|
splitLine.IsVisible.ShouldBeFalse();
|
|
splitLine.BorderThickness.ShouldBe(new Thickness(0));
|
|
item.EffectiveBorderThickness.ShouldBe(new Thickness(0));
|
|
item.IsSplitLineEffectiveVisible.ShouldBeFalse();
|
|
}
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Root_Border_Uses_The_Item_Divider_Color()
|
|
{
|
|
var listBox = new AtomListBox
|
|
{
|
|
ItemsSource = CreateItems("Olivia")
|
|
};
|
|
|
|
ShowInWindow(listBox, () =>
|
|
{
|
|
var rootBorderColor = ResolveColor(GetFrame(listBox).BorderBrush);
|
|
var itemBorderColor = ResolveColor(GetSemanticItems(listBox).Single().BorderBrush);
|
|
|
|
rootBorderColor.ShouldBe(itemBorderColor);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Item_Surfaces_Are_Square_Across_Size_Types()
|
|
{
|
|
foreach (var sizeType in new[]
|
|
{
|
|
CustomizableSizeType.Large,
|
|
CustomizableSizeType.Middle,
|
|
CustomizableSizeType.Small
|
|
})
|
|
{
|
|
var listBox = new AtomListBox
|
|
{
|
|
SizeType = sizeType,
|
|
ItemsSource = CreateItems("Olivia")
|
|
};
|
|
|
|
ShowInWindow(listBox, () =>
|
|
{
|
|
GetItemFrame(GetSemanticItems(listBox).Single())
|
|
.CornerRadius.ShouldBe(new CornerRadius(0));
|
|
});
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Items_Touch_The_Outer_Border_Without_Content_Padding()
|
|
{
|
|
var listBox = new AtomListBox
|
|
{
|
|
ItemsSource = CreateItems("Olivia")
|
|
};
|
|
|
|
ShowInWindow(listBox, () =>
|
|
{
|
|
listBox.Padding.ShouldBe(new Thickness(0));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Items_Sit_Flush_Without_Vertical_Margins()
|
|
{
|
|
var listBox = new AtomListBox
|
|
{
|
|
ItemsSource = CreateItems("Olivia", "Liam")
|
|
};
|
|
|
|
ShowInWindow(listBox, () =>
|
|
{
|
|
GetSemanticItems(listBox).ShouldAllBe(static item => item.Margin == new Thickness(0));
|
|
});
|
|
}
|
|
|
|
private static ListItemData[] CreateItems(params string[] values)
|
|
{
|
|
return values.Select(value => new ListItemData { Content = value }).ToArray();
|
|
}
|
|
|
|
private static Style CreateRouteStyle()
|
|
{
|
|
return new Style(selector =>
|
|
selector.OfType<AtomListBox>()
|
|
.Class("semantic-owner")
|
|
.Child()
|
|
.Class(ItemClass))
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, "item") }
|
|
};
|
|
}
|
|
|
|
private static AtomListBoxItem[] GetSemanticItems(AtomListBox listBox)
|
|
{
|
|
return GetContainers(listBox)
|
|
.Where(static item => item.Classes.Contains(ItemClass))
|
|
.ToArray();
|
|
}
|
|
|
|
private static AtomListBoxItem[] GetContainers(AtomListBox listBox)
|
|
{
|
|
return listBox.GetVisualDescendants()
|
|
.OfType<AtomListBoxItem>()
|
|
.ToArray();
|
|
}
|
|
|
|
private static PixelAlignedBorder GetItemFrame(AtomListBoxItem item)
|
|
{
|
|
return item.GetVisualDescendants()
|
|
.OfType<PixelAlignedBorder>()
|
|
.Single(static border => border.Name == "Frame");
|
|
}
|
|
|
|
private static PixelAlignedBorder GetSplitLineFrame(AtomListBoxItem item)
|
|
{
|
|
return item.GetVisualDescendants()
|
|
.OfType<PixelAlignedBorder>()
|
|
.Single(static border => border.Name == "SplitLineFrame");
|
|
}
|
|
|
|
private static PixelAlignedBorder GetFrame(AtomListBox listBox)
|
|
{
|
|
return listBox.GetVisualDescendants()
|
|
.OfType<PixelAlignedBorder>()
|
|
.First(static border => border.Name == "Frame");
|
|
}
|
|
|
|
private static Color ResolveColor(IBrush? brush)
|
|
{
|
|
var immutable = brush.ShouldNotBeNull().ToImmutable();
|
|
return immutable.ShouldBeAssignableTo<IImmutableSolidColorBrush>().Color;
|
|
}
|
|
|
|
private static void AssertRectClose(Rect actual, Rect expected)
|
|
{
|
|
const double tolerance = 0.01;
|
|
Math.Abs(actual.X - expected.X).ShouldBeLessThan(tolerance);
|
|
Math.Abs(actual.Y - expected.Y).ShouldBeLessThan(tolerance);
|
|
Math.Abs(actual.Width - expected.Width).ShouldBeLessThan(tolerance);
|
|
Math.Abs(actual.Height - expected.Height).ShouldBeLessThan(tolerance);
|
|
}
|
|
|
|
private static void ShowInWindow(Control content, Action assertion)
|
|
{
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 800,
|
|
Height = 600,
|
|
Content = content
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
assertion();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
private static void AssertRoot(SemanticPartDescriptor part)
|
|
{
|
|
part.Path.ShouldBe("root");
|
|
part.SelectorClass.ShouldBeNull();
|
|
part.ContractType.ShouldBe(typeof(AtomListBox));
|
|
part.Cardinality.ShouldBe(SemanticPartCardinality.Single);
|
|
part.Customization.ShouldBe(SemanticPartCustomization.Root);
|
|
part.CrossVisualRoot.ShouldBeFalse();
|
|
part.RuntimeCreated.ShouldBeFalse();
|
|
}
|
|
|
|
private static void AssertPart(
|
|
SemanticPartDescriptor part,
|
|
string selectorClass,
|
|
Type contractType,
|
|
string selectorRoute)
|
|
{
|
|
part.Path.ShouldBe(part.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.ShouldBeFalse();
|
|
part.RuntimeCreated.ShouldBeTrue();
|
|
part.Since.ShouldBe("6.2.0");
|
|
}
|
|
}
|