mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-28 23:08:36 +08:00
620 lines
21 KiB
C#
620 lines
21 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Reflection;
|
|
using AtomUI.Controls.Data;
|
|
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 AtomUI.Controls.Primitives;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AtomListView = AtomUI.Desktop.Controls.ListView;
|
|
using AtomListViewItem = AtomUI.Desktop.Controls.ListViewItem;
|
|
using AtomPagination = AtomUI.Desktop.Controls.Pagination;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.ListView;
|
|
|
|
public class ListViewSemanticPartTests
|
|
{
|
|
private const string ItemClass = "semantic-item";
|
|
private const string GroupHeaderClass = "semantic-group-header";
|
|
private const string ItemRoute = "> .semantic-item";
|
|
private const string GroupHeaderRoute = "> .semantic-group-header";
|
|
|
|
static ListViewSemanticPartTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void Registered_Descriptor_Exposes_Only_The_Approved_Parts()
|
|
{
|
|
var registry = Application.Current.ShouldNotBeNull()
|
|
.GetThemeManager().ShouldNotBeNull()
|
|
.SemanticParts;
|
|
|
|
registry.TryGetControl(typeof(AtomListView), out var descriptor).ShouldBeTrue();
|
|
descriptor.ShouldNotBeNull();
|
|
descriptor.Parts.Select(static part => part.Name)
|
|
.ShouldBe(["root", "groupHeader", "item"]);
|
|
|
|
AssertRoot(descriptor.Parts.Single(static part => part.Name == "root"));
|
|
AssertPart(descriptor.Parts.Single(static part => part.Name == "item"),
|
|
ItemClass,
|
|
typeof(AtomListViewItem),
|
|
ItemRoute);
|
|
AssertPart(descriptor.Parts.Single(static part => part.Name == "groupHeader"),
|
|
GroupHeaderClass,
|
|
typeof(AtomListViewItem),
|
|
GroupHeaderRoute);
|
|
}
|
|
|
|
[Fact]
|
|
public void Grouped_Items_Produce_Item_And_Group_Header_Containers()
|
|
{
|
|
var listView = new AtomListView
|
|
{
|
|
IsGroupEnabled = true,
|
|
ItemsSource = new ListItemData[]
|
|
{
|
|
CreateItem("Olivia", "Design"),
|
|
CreateItem("Liam", "Design"),
|
|
CreateItem("Emma", "Engineering")
|
|
}
|
|
};
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
GetSemanticItems(listView).Length.ShouldBe(3);
|
|
GetSemanticGroupHeaders(listView).Length.ShouldBe(2);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Group_And_Item_Containers_Use_Distinct_Recycle_Keys()
|
|
{
|
|
var listView = new AtomListView();
|
|
|
|
var groupKey = ResolveNeedsContainer(listView, CreateGroupHeader("Design"));
|
|
var itemKey = ResolveNeedsContainer(listView, CreateItem("Olivia"));
|
|
|
|
groupKey.NeedsContainer.ShouldBeTrue();
|
|
itemKey.NeedsContainer.ShouldBeTrue();
|
|
groupKey.RecycleKey.ShouldNotBeNull();
|
|
itemKey.RecycleKey.ShouldNotBeNull();
|
|
groupKey.RecycleKey.ShouldNotBe(itemKey.RecycleKey,
|
|
"group header containers must never be recycled as item containers.");
|
|
}
|
|
|
|
[Fact]
|
|
public void Prepare_And_Clear_Cycles_Do_Not_Toggle_Markers()
|
|
{
|
|
var listView = new AtomListView
|
|
{
|
|
IsGroupEnabled = true,
|
|
ItemsSource = new ListItemData[]
|
|
{
|
|
CreateItem("Olivia", "Design"),
|
|
CreateItem("Liam", "Engineering")
|
|
}
|
|
};
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
var item = GetSemanticItems(listView)[0];
|
|
var groupHeader = GetSemanticGroupHeaders(listView)[0];
|
|
|
|
var itemIndex = listView.IndexFromContainer(item);
|
|
var groupIndex = listView.IndexFromContainer(groupHeader);
|
|
var itemData = listView.Items[itemIndex];
|
|
var groupData = listView.Items[groupIndex];
|
|
|
|
ClearContainerForItem(listView, item);
|
|
PrepareContainerForItem(listView, item, itemData!, itemIndex);
|
|
ClearContainerForItem(listView, groupHeader);
|
|
PrepareContainerForItem(listView, groupHeader, groupData!, groupIndex);
|
|
|
|
item.Classes.ShouldContain(ItemClass);
|
|
item.Classes.ShouldNotContain(GroupHeaderClass);
|
|
groupHeader.Classes.ShouldContain(GroupHeaderClass);
|
|
groupHeader.Classes.ShouldNotContain(ItemClass);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Divider_Uses_Current_View_Position_For_Repeated_Items()
|
|
{
|
|
var repeated = new ListItemData { Content = "same" };
|
|
var listView = new AtomListView
|
|
{
|
|
ItemsSource = new[] { repeated, new ListItemData { Content = "other" }, repeated }
|
|
};
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
Enumerable.Range(0, 3)
|
|
.Select(index => GetSplitLineFrame(GetSemanticItems(listView)[index]).IsVisible)
|
|
.ShouldBe([true, true, false]);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Root_Semantic_Part_Projects_The_Standard_Surface_Properties()
|
|
{
|
|
var listView = new AtomListView
|
|
{
|
|
ItemsSource = new ListItemData[] { CreateItem("Olivia") }
|
|
};
|
|
listView.Background = new SolidColorBrush(Colors.AliceBlue);
|
|
listView.BorderBrush = new SolidColorBrush(Color.Parse("#CDC1FF"));
|
|
listView.BorderThickness = new Thickness(1);
|
|
listView.CornerRadius = new CornerRadius(8);
|
|
listView.Padding = new Thickness(10);
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
var frame = GetFrame(listView);
|
|
|
|
frame.Background.ShouldBeSameAs(listView.Background);
|
|
frame.BorderBrush.ShouldBeSameAs(listView.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 listView = new AtomListView
|
|
{
|
|
IsGroupEnabled = true,
|
|
ItemsSource = new ListItemData[]
|
|
{
|
|
CreateItem("Olivia", "Design")
|
|
}
|
|
};
|
|
listView.Classes.Add("semantic-owner");
|
|
listView.Styles.Add(CreateRouteStyle(ItemClass, "item"));
|
|
listView.Styles.Add(CreateRouteStyle(GroupHeaderClass, "group"));
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
GetSemanticItems(listView).ShouldAllBe(static item => Equals(item.Tag, "item"));
|
|
GetSemanticGroupHeaders(listView).ShouldAllBe(static header => Equals(header.Tag, "group"));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Item_Containers_Get_A_Bottom_Divider_And_Group_Headers_Do_Not()
|
|
{
|
|
var listView = new AtomListView
|
|
{
|
|
IsGroupEnabled = true,
|
|
ItemsSource = new ListItemData[]
|
|
{
|
|
CreateItem("Olivia", "Design"),
|
|
CreateItem("Liam", "Engineering")
|
|
}
|
|
};
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
var items = GetSemanticItems(listView);
|
|
var itemSplitLines = items.Select(GetSplitLineFrame).ToArray();
|
|
var groupSplitLines = GetSemanticGroupHeaders(listView).Select(GetSplitLineFrame).ToArray();
|
|
|
|
itemSplitLines.Length.ShouldBe(2);
|
|
groupSplitLines.Length.ShouldBe(2);
|
|
// Every item keeps its divider except the last one, which is closed by the root border.
|
|
itemSplitLines[0].IsVisible.ShouldBeTrue();
|
|
itemSplitLines[0].BorderThickness.Bottom.ShouldBe(1);
|
|
itemSplitLines[1].IsVisible.ShouldBeFalse();
|
|
groupSplitLines.ShouldAllBe(static line => !line.IsVisible);
|
|
var firstItemBackground = GetItemFrame(items[0]).Background;
|
|
GetSemanticGroupHeaders(listView).Select(GetItemFrame)
|
|
.ShouldAllBe(frame => !Equals(frame.Background, firstItemBackground));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Semantic_Item_Border_Style_Overrides_The_Default_Divider()
|
|
{
|
|
var listView = new AtomListView
|
|
{
|
|
ItemsSource = new ListItemData[] { CreateItem("Olivia"), CreateItem("Liam") }
|
|
};
|
|
listView.Classes.Add("semantic-owner");
|
|
listView.Styles.Add(new Style(selector =>
|
|
selector.OfType<AtomListView>()
|
|
.Class("semantic-owner")
|
|
.Child()
|
|
.Class(ItemClass))
|
|
{
|
|
Setters = { new Setter(TemplatedControl.BorderThicknessProperty, new Thickness(2)) }
|
|
});
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
var items = GetSemanticItems(listView);
|
|
var styledSplitLine = GetSplitLineFrame(items[0]);
|
|
styledSplitLine.IsVisible.ShouldBeTrue();
|
|
styledSplitLine.BorderThickness.ShouldBe(new Thickness(2));
|
|
GetSplitLineFrame(items[1]).IsVisible.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Only_The_Last_Item_Suppresses_The_Divider_Without_Pagination()
|
|
{
|
|
var listView = new AtomListView
|
|
{
|
|
ItemsSource = new ListItemData[]
|
|
{
|
|
CreateItem("Olivia"),
|
|
CreateItem("Liam"),
|
|
CreateItem("Emma")
|
|
}
|
|
};
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
var splitLines = GetSemanticItems(listView).Select(GetSplitLineFrame).ToArray();
|
|
splitLines[0].IsVisible.ShouldBeTrue();
|
|
splitLines[1].IsVisible.ShouldBeTrue();
|
|
splitLines[2].IsVisible.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Last_Item_Keeps_The_Divider_When_Bottom_Pagination_Is_Present()
|
|
{
|
|
var listView = new AtomListView
|
|
{
|
|
ItemsSource = new ListItemData[] { CreateItem("Olivia"), CreateItem("Liam") },
|
|
BottomPagination = new AtomPagination()
|
|
};
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
GetSemanticItems(listView).Select(GetSplitLineFrame)
|
|
.ShouldAllBe(static line => line.IsVisible);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Borderless_Keeps_The_Divider_On_The_Last_Item()
|
|
{
|
|
var listView = new AtomListView
|
|
{
|
|
IsBorderless = true,
|
|
ItemsSource = new ListItemData[] { CreateItem("Olivia"), CreateItem("Liam") }
|
|
};
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
GetSemanticItems(listView).Select(GetSplitLineFrame)
|
|
.ShouldAllBe(static line => line.IsVisible);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Appending_An_Item_Restores_The_Previously_Last_Items_Divider()
|
|
{
|
|
var items = new ObservableCollection<ListItemData>
|
|
{
|
|
CreateItem("Olivia"),
|
|
CreateItem("Liam")
|
|
};
|
|
var listView = new AtomListView
|
|
{
|
|
ItemsSource = items
|
|
};
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
GetSplitLineFrame(GetSemanticItems(listView)[1]).IsVisible.ShouldBeFalse();
|
|
|
|
items.Add(CreateItem("Emma"));
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var splitLines = GetSemanticItems(listView).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 listView = new AtomListView
|
|
{
|
|
ItemsSource = new ListItemData[] { CreateItem("Olivia") }
|
|
};
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
var frame = GetFrame(listView);
|
|
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 Select_And_Transfer_Items_Do_Not_Inherit_The_List_Divider()
|
|
{
|
|
var selectList = new SelectCandidateList
|
|
{
|
|
ItemsSource = new ISelectOption[]
|
|
{
|
|
new SelectOption { Header = "Time", Content = "Time" },
|
|
new SelectOption { Header = "Date", Content = "Date" }
|
|
}
|
|
};
|
|
var transferItem = new TransferListItem();
|
|
|
|
ShowInWindow(new StackPanel { Children = { selectList, transferItem } }, () =>
|
|
{
|
|
var selectItems = selectList.GetVisualDescendants().OfType<SelectCandidateListItem>().ToArray();
|
|
selectItems.Length.ShouldBe(2);
|
|
foreach (var selectItem in selectItems)
|
|
{
|
|
var selectSplitLine = GetSplitLineFrame(selectItem);
|
|
selectSplitLine.IsVisible.ShouldBeFalse();
|
|
selectSplitLine.BorderThickness.ShouldBe(new Thickness(0));
|
|
selectItem.EffectiveBorderThickness.ShouldBe(new Thickness(0));
|
|
selectItem.IsSplitLineEffectiveVisible.ShouldBeFalse();
|
|
}
|
|
|
|
transferItem.GetVisualDescendants()
|
|
.OfType<PixelAlignedBorder>()
|
|
.ShouldNotContain(static border => border.Name == "SplitLineFrame");
|
|
|
|
transferItem.EffectiveBorderThickness.ShouldBe(new Thickness(0));
|
|
transferItem.IsSplitLineEffectiveVisible.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Root_Border_Uses_The_Item_Divider_Color()
|
|
{
|
|
var listView = new AtomListView
|
|
{
|
|
ItemsSource = new ListItemData[] { CreateItem("Olivia") }
|
|
};
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
var rootBorderColor = ResolveColor(GetFrame(listView).BorderBrush);
|
|
var itemBorderColor = ResolveColor(GetSemanticItems(listView).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 listView = new AtomListView
|
|
{
|
|
SizeType = sizeType,
|
|
ItemsSource = new ListItemData[] { CreateItem("Olivia") }
|
|
};
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
GetItemFrame(GetSemanticItems(listView).Single())
|
|
.CornerRadius.ShouldBe(new CornerRadius(0));
|
|
});
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Items_Touch_The_Outer_Border_Without_Content_Padding()
|
|
{
|
|
var listView = new AtomListView
|
|
{
|
|
ItemsSource = new ListItemData[] { CreateItem("Olivia") }
|
|
};
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
listView.Padding.ShouldBe(new Thickness(0));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Items_Sit_Flush_Without_Vertical_Margins()
|
|
{
|
|
var listView = new AtomListView
|
|
{
|
|
ItemsSource = new ListItemData[]
|
|
{
|
|
CreateItem("Olivia"),
|
|
CreateItem("Liam")
|
|
}
|
|
};
|
|
|
|
ShowInWindow(listView, () =>
|
|
{
|
|
GetSemanticItems(listView).ShouldAllBe(static item => item.Margin == new Thickness(0));
|
|
});
|
|
}
|
|
|
|
private static ListItemData CreateItem(string content, string? group = null)
|
|
{
|
|
return new ListItemData { Content = content, Group = group };
|
|
}
|
|
|
|
private static GroupListItemData CreateGroupHeader(string content)
|
|
{
|
|
return new GroupListItemData { Content = content, IsGroupItem = true };
|
|
}
|
|
|
|
private static Style CreateRouteStyle(string partClass, string tag)
|
|
{
|
|
return new Style(selector =>
|
|
selector.OfType<AtomListView>()
|
|
.Class("semantic-owner")
|
|
.Child()
|
|
.Class(partClass))
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, tag) }
|
|
};
|
|
}
|
|
|
|
private static AtomListViewItem[] GetSemanticItems(AtomListView listView)
|
|
{
|
|
return listView.GetVisualDescendants()
|
|
.OfType<AtomListViewItem>()
|
|
.Where(static item => item.Classes.Contains(ItemClass))
|
|
.ToArray();
|
|
}
|
|
|
|
private static AtomListViewItem[] GetSemanticGroupHeaders(AtomListView listView)
|
|
{
|
|
return listView.GetVisualDescendants()
|
|
.OfType<AtomListViewItem>()
|
|
.Where(static item => item.Classes.Contains(GroupHeaderClass))
|
|
.ToArray();
|
|
}
|
|
|
|
private static PixelAlignedBorder GetItemFrame(AtomListViewItem item)
|
|
{
|
|
return item.GetVisualDescendants()
|
|
.OfType<PixelAlignedBorder>()
|
|
.Single(static border => border.Name == "Frame");
|
|
}
|
|
|
|
private static PixelAlignedBorder GetSplitLineFrame(AtomListViewItem item)
|
|
{
|
|
return item.GetVisualDescendants()
|
|
.OfType<PixelAlignedBorder>()
|
|
.Single(static border => border.Name == "SplitLineFrame");
|
|
}
|
|
|
|
private static PixelAlignedBorder GetFrame(AtomListView listView)
|
|
{
|
|
return listView.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 (bool NeedsContainer, object? RecycleKey) ResolveNeedsContainer(
|
|
AtomListView listView,
|
|
object? item)
|
|
{
|
|
var method = typeof(AtomListView).GetMethod(
|
|
"NeedsContainerOverride",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
method.ShouldNotBeNull();
|
|
var args = new object?[] { item, 0, null };
|
|
var needsContainer = (bool)method.Invoke(listView, args)!;
|
|
return (needsContainer, args[2]);
|
|
}
|
|
|
|
private static void PrepareContainerForItem(
|
|
AtomListView listView,
|
|
AtomListViewItem container,
|
|
object item,
|
|
int index)
|
|
{
|
|
var method = typeof(AtomListView).GetMethod(
|
|
"PrepareContainerForItemOverride",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
method.ShouldNotBeNull();
|
|
method.Invoke(listView, [container, item, index]);
|
|
}
|
|
|
|
private static void ClearContainerForItem(
|
|
AtomListView listView,
|
|
AtomListViewItem container)
|
|
{
|
|
var method = typeof(AtomListView).GetMethod(
|
|
"ClearContainerForItemOverride",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
method.ShouldNotBeNull();
|
|
method.Invoke(listView, [container]);
|
|
}
|
|
|
|
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(AtomListView));
|
|
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");
|
|
}
|
|
}
|