mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-26 03:00:28 +08:00
596 lines
22 KiB
C#
596 lines
22 KiB
C#
using System.Xml.Linq;
|
|
using AtomUI.Theme;
|
|
using AtomUI.Theme.Schema;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Presenters;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Media;
|
|
using Avalonia.Styling;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using AtomUI.Controls.Primitives;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AtomUICollapse = AtomUI.Desktop.Controls.Collapse;
|
|
using AtomUICollapseItem = AtomUI.Desktop.Controls.CollapseItem;
|
|
using IconButtonControl = AtomUI.Desktop.Controls.IconButton;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.CollapseControl;
|
|
|
|
public class CollapseSemanticPartTests
|
|
{
|
|
private const string HeaderClass = "semantic-header";
|
|
private const string IconClass = "semantic-icon";
|
|
private const string TitleClass = "semantic-title";
|
|
private const string BodyClass = "semantic-body";
|
|
private const string ScopeItemsClass = "semantic-scope-items";
|
|
private const string ScopePanelClass = "semantic-scope-panel";
|
|
private const string ScopeItemClass = "semantic-scope-item";
|
|
private const string RoutePrefix = "> .semantic-scope-item /template/ .";
|
|
|
|
static CollapseSemanticPartTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void Registered_Descriptor_Exposes_Only_The_Approved_Parts()
|
|
{
|
|
var registry = Application.Current.ShouldNotBeNull()
|
|
.GetThemeManager().ShouldNotBeNull()
|
|
.SemanticParts;
|
|
|
|
registry.TryGetControl(typeof(AtomUICollapse), out var descriptor).ShouldBeTrue();
|
|
descriptor.ShouldNotBeNull();
|
|
descriptor.Parts.Select(static part => part.Name)
|
|
.ShouldBe(["root", "body", "header", "icon", "title"]);
|
|
|
|
AssertRoot(descriptor.Parts.Single(static part => part.Name == "root"));
|
|
AssertPart(descriptor.Parts.Single(static part => part.Name == "header"),
|
|
HeaderClass,
|
|
typeof(PixelAlignedBorder),
|
|
RoutePrefix + HeaderClass);
|
|
AssertPart(descriptor.Parts.Single(static part => part.Name == "icon"),
|
|
IconClass,
|
|
typeof(IconButtonControl),
|
|
RoutePrefix + IconClass);
|
|
AssertPart(descriptor.Parts.Single(static part => part.Name == "title"),
|
|
TitleClass,
|
|
typeof(ContentPresenter),
|
|
RoutePrefix + TitleClass);
|
|
AssertPart(descriptor.Parts.Single(static part => part.Name == "body"),
|
|
BodyClass,
|
|
typeof(PixelAlignedBorder),
|
|
RoutePrefix + BodyClass);
|
|
|
|
registry.TryGetControl(typeof(AtomUICollapseItem), out _).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Templates_Implement_Only_The_Approved_Static_Markers()
|
|
{
|
|
AssertThemeMarkers(
|
|
"src/AtomUI.Desktop.Controls/Collapse/Themes/CollapseTheme.axaml",
|
|
["semantic-scope-items:ItemsPresenter"]);
|
|
AssertThemeMarkers(
|
|
"src/AtomUI.Desktop.Controls/Collapse/Themes/CollapseItemTheme.axaml",
|
|
[
|
|
"semantic-body:PixelAlignedBorder",
|
|
"semantic-header:PixelAlignedBorder",
|
|
"semantic-icon:IconButton",
|
|
"semantic-title:ContentPresenter"
|
|
]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Panels_Produce_One_Part_Marker_Set_Per_Item_And_A_Single_Scope_Chain()
|
|
{
|
|
var collapse = CreateCollapseWithThreePanels();
|
|
|
|
ShowInWindow(collapse, () =>
|
|
{
|
|
// The body marker lives inside the content motion actor: it is materialized
|
|
// only while the panel is expanded, so expand every panel before counting.
|
|
ExpandAllPanels(collapse);
|
|
GetSemanticHeaders(collapse).Length.ShouldBe(3);
|
|
GetSemanticIcons(collapse).Length.ShouldBe(3);
|
|
GetSemanticTitles(collapse).Length.ShouldBe(3);
|
|
GetSemanticBodies(collapse).Length.ShouldBe(3);
|
|
|
|
var presenter = GetItemsPresenter(collapse);
|
|
presenter.Classes.ShouldContain(ScopeItemsClass);
|
|
var panel = GetItemsPanel(collapse);
|
|
panel.Classes.ShouldContain(ScopePanelClass);
|
|
panel.ShouldBeSameAs(presenter.Panel);
|
|
GetScopeItemContainers(collapse).Length.ShouldBe(3);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Body_Marker_Follows_Panel_Expansion_State()
|
|
{
|
|
var collapse = CreateCollapseWithThreePanels();
|
|
|
|
ShowInWindow(collapse, () =>
|
|
{
|
|
GetSemanticHeaders(collapse).Length.ShouldBe(3);
|
|
GetSemanticTitles(collapse).Length.ShouldBe(3);
|
|
GetSemanticBodies(collapse).Length.ShouldBe(1);
|
|
|
|
ExpandAllPanels(collapse);
|
|
GetSemanticBodies(collapse).Length.ShouldBe(3);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Empty_Collection_Keeps_The_Scope_Chain_But_Has_No_Part_Markers()
|
|
{
|
|
var collapse = new AtomUICollapse();
|
|
|
|
ShowInWindow(collapse, () =>
|
|
{
|
|
GetItemsPresenter(collapse).Classes.ShouldContain(ScopeItemsClass);
|
|
GetItemsPanel(collapse).Classes.ShouldContain(ScopePanelClass);
|
|
GetSemanticHeaders(collapse).ShouldBeEmpty();
|
|
GetSemanticIcons(collapse).ShouldBeEmpty();
|
|
GetSemanticTitles(collapse).ShouldBeEmpty();
|
|
GetSemanticBodies(collapse).ShouldBeEmpty();
|
|
GetScopeItemContainers(collapse).ShouldBeEmpty();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Items_Clear_And_ReAdd_Tracks_The_Part_Markers()
|
|
{
|
|
var collapse = CreateCollapseWithThreePanels();
|
|
|
|
ShowInWindow(collapse, () =>
|
|
{
|
|
GetSemanticHeaders(collapse).Length.ShouldBe(3);
|
|
|
|
collapse.Items.Clear();
|
|
Dispatcher.UIThread.RunJobs();
|
|
GetSemanticHeaders(collapse).ShouldBeEmpty();
|
|
GetScopeItemContainers(collapse).ShouldBeEmpty();
|
|
GetItemsPanel(collapse).Classes.ShouldContain(ScopePanelClass);
|
|
|
|
collapse.Items.Add(CreateItem("1", true));
|
|
collapse.Items.Add(CreateItem("2", true));
|
|
Dispatcher.UIThread.RunJobs();
|
|
GetSemanticHeaders(collapse).Length.ShouldBe(2);
|
|
GetSemanticBodies(collapse).Length.ShouldBe(2);
|
|
GetScopeItemContainers(collapse).Length.ShouldBe(2);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void ShowExpandIcon_False_Hides_The_Icon_But_Preserves_The_Marker()
|
|
{
|
|
var collapse = new AtomUICollapse
|
|
{
|
|
Items =
|
|
{
|
|
CreateItem("1", true),
|
|
new AtomUICollapseItem
|
|
{
|
|
Header = "Header 2",
|
|
Content = "Content 2",
|
|
IsSelected = false,
|
|
IsShowExpandIcon = false
|
|
}
|
|
}
|
|
};
|
|
|
|
ShowInWindow(collapse, () =>
|
|
{
|
|
var icons = GetSemanticIcons(collapse);
|
|
icons.Length.ShouldBe(2);
|
|
icons.ShouldAllBe(static icon => icon.Classes.Contains(IconClass));
|
|
icons.Single(static icon => !icon.IsVisible);
|
|
icons.Single(static icon => icon.IsVisible);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void State_And_Visual_Mode_Switches_Do_Not_Change_Marker_Counts()
|
|
{
|
|
var collapse = CreateCollapseWithThreePanels();
|
|
|
|
ShowInWindow(collapse, () =>
|
|
{
|
|
ExpandAllPanels(collapse);
|
|
AssertPanelMarkerCounts(collapse, 3, 3);
|
|
|
|
collapse.SizeType = CustomizableSizeType.Large;
|
|
collapse.ExpandIconPosition = CollapseExpandIconPosition.End;
|
|
collapse.IsBorderless = true;
|
|
collapse.IsGhostStyle = true;
|
|
collapse.IsAccordion = true;
|
|
collapse.TriggerType = CollapseTriggerType.Icon;
|
|
Dispatcher.UIThread.RunJobs();
|
|
AssertPanelMarkerCounts(collapse, 3, 3);
|
|
|
|
foreach (var item in GetScopeItemContainers(collapse))
|
|
{
|
|
item.IsSelected = !item.IsSelected;
|
|
}
|
|
Dispatcher.UIThread.RunJobs();
|
|
ExpandAllPanels(collapse);
|
|
AssertPanelMarkerCounts(collapse, 3, 3);
|
|
|
|
collapse.IsGhostStyle = false;
|
|
collapse.IsBorderless = false;
|
|
collapse.IsAccordion = false;
|
|
Dispatcher.UIThread.RunJobs();
|
|
AssertPanelMarkerCounts(collapse, 3, 3);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Detach_And_Reattach_Regenerates_The_Scope_Chain_And_Keeps_Every_Panel_Marked()
|
|
{
|
|
var collapse = CreateCollapseWithThreePanels();
|
|
var window = new AvaloniaWindow { Width = 800, Height = 600 };
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
window.Content = collapse;
|
|
Dispatcher.UIThread.RunJobs();
|
|
ExpandAllPanels(collapse);
|
|
AssertPanelMarkerCounts(collapse, 3, 3);
|
|
|
|
window.Content = null;
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.Content = collapse;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
ExpandAllPanels(collapse);
|
|
AssertPanelMarkerCounts(collapse, 3, 3);
|
|
GetItemsPresenter(collapse).Classes.ShouldContain(ScopeItemsClass);
|
|
GetItemsPanel(collapse).Classes.ShouldContain(ScopePanelClass);
|
|
GetScopeItemContainers(collapse).Length.ShouldBe(3);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Root_Semantic_Part_Projects_The_Standard_Surface_Properties()
|
|
{
|
|
var collapse = CreateCollapseWithThreePanels();
|
|
collapse.Background = new SolidColorBrush(Colors.AliceBlue);
|
|
collapse.BorderBrush = new SolidColorBrush(Color.Parse("#CDC1FF"));
|
|
collapse.BorderThickness = new Thickness(1);
|
|
collapse.CornerRadius = new CornerRadius(8);
|
|
collapse.Padding = new Thickness(10);
|
|
|
|
ShowInWindow(collapse, () =>
|
|
{
|
|
var frame = GetFrame(collapse);
|
|
|
|
frame.Background.ShouldBeSameAs(collapse.Background);
|
|
frame.BorderBrush.ShouldBeSameAs(collapse.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 collapse = CreateCollapseWithThreePanels();
|
|
collapse.Classes.Add("semantic-owner");
|
|
collapse.Styles.Add(CreateRouteStyle(HeaderClass, "header"));
|
|
collapse.Styles.Add(CreateRouteStyle(IconClass, "icon"));
|
|
collapse.Styles.Add(CreateRouteStyle(TitleClass, "title"));
|
|
collapse.Styles.Add(CreateRouteStyle(BodyClass, "body"));
|
|
|
|
ShowInWindow(collapse, () =>
|
|
{
|
|
GetSemanticHeaders(collapse).ShouldAllBe(static header => Equals(header.Tag, "header"));
|
|
GetSemanticIcons(collapse).ShouldAllBe(static icon => Equals(icon.Tag, "icon"));
|
|
GetSemanticTitles(collapse).ShouldAllBe(static title => Equals(title.Tag, "title"));
|
|
GetSemanticBodies(collapse).ShouldAllBe(static body => Equals(body.Tag, "body"));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Semantic_Header_Padding_Overrides_The_Large_Size_Tier_And_Shrinks_The_Header()
|
|
{
|
|
var baseline = new AtomUICollapse
|
|
{
|
|
SizeType = CustomizableSizeType.Large,
|
|
Items = { CreateItem("1", true) }
|
|
};
|
|
var styled = new AtomUICollapse
|
|
{
|
|
SizeType = CustomizableSizeType.Large,
|
|
Items = { CreateItem("1", true) }
|
|
};
|
|
styled.Classes.Add("semantic-owner");
|
|
styled.Styles.Add(new Style(selector =>
|
|
selector.OfType<AtomUICollapse>()
|
|
.Class("semantic-owner")
|
|
.Child()
|
|
.Class(ScopeItemClass)
|
|
.Template()
|
|
.Class(HeaderClass))
|
|
{
|
|
Setters = { new Setter(Border.PaddingProperty, new Thickness(16, 12)) }
|
|
});
|
|
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 800,
|
|
Height = 600,
|
|
Content = new StackPanel
|
|
{
|
|
Orientation = Orientation.Vertical,
|
|
Children = { baseline, styled }
|
|
}
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var baselineHeader = GetSemanticHeaders(baseline).Single();
|
|
var styledHeader = GetSemanticHeaders(styled).Single();
|
|
styledHeader.Padding.ShouldBe(new Thickness(16, 12));
|
|
styledHeader.Padding.ShouldNotBe(baselineHeader.Padding);
|
|
styledHeader.Bounds.Height.ShouldBeLessThan(baselineHeader.Bounds.Height);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SizeType_Tiers_Project_Distinct_Header_Paddings()
|
|
{
|
|
var collapse = new AtomUICollapse
|
|
{
|
|
Items = { CreateItem("1", true) }
|
|
};
|
|
|
|
ShowInWindow(collapse, () =>
|
|
{
|
|
collapse.SizeType = CustomizableSizeType.Small;
|
|
Dispatcher.UIThread.RunJobs();
|
|
var smallPadding = GetSemanticHeaders(collapse).Single().Padding;
|
|
|
|
collapse.SizeType = CustomizableSizeType.Middle;
|
|
Dispatcher.UIThread.RunJobs();
|
|
var middlePadding = GetSemanticHeaders(collapse).Single().Padding;
|
|
|
|
collapse.SizeType = CustomizableSizeType.Large;
|
|
Dispatcher.UIThread.RunJobs();
|
|
var largePadding = GetSemanticHeaders(collapse).Single().Padding;
|
|
|
|
smallPadding.ShouldNotBe(largePadding);
|
|
middlePadding.ShouldNotBe(largePadding);
|
|
});
|
|
}
|
|
|
|
private static AtomUICollapse CreateCollapseWithThreePanels()
|
|
{
|
|
return new AtomUICollapse
|
|
{
|
|
IsMotionEnabled = false,
|
|
Items =
|
|
{
|
|
CreateItem("1", true),
|
|
CreateItem("2", false),
|
|
CreateItem("3", false)
|
|
}
|
|
};
|
|
}
|
|
|
|
private static AtomUICollapseItem CreateItem(string header, bool isSelected)
|
|
{
|
|
return new AtomUICollapseItem
|
|
{
|
|
Header = $"Header {header}",
|
|
Content = $"Content {header}",
|
|
IsSelected = isSelected
|
|
};
|
|
}
|
|
|
|
private static Style CreateRouteStyle(string partClass, string tag)
|
|
{
|
|
return new Style(selector =>
|
|
selector.OfType<AtomUICollapse>()
|
|
.Class("semantic-owner")
|
|
.Child()
|
|
.Class(ScopeItemClass)
|
|
.Template()
|
|
.Class(partClass))
|
|
{
|
|
Setters = { new Setter(Control.TagProperty, tag) }
|
|
};
|
|
}
|
|
|
|
private static void AssertPanelMarkerCounts(AtomUICollapse collapse, int expectedPanels, int expectedBodies)
|
|
{
|
|
GetSemanticHeaders(collapse).Length.ShouldBe(expectedPanels);
|
|
GetSemanticIcons(collapse).Length.ShouldBe(expectedPanels);
|
|
GetSemanticTitles(collapse).Length.ShouldBe(expectedPanels);
|
|
GetSemanticBodies(collapse).Length.ShouldBe(expectedBodies);
|
|
GetScopeItemContainers(collapse).Length.ShouldBe(expectedPanels);
|
|
}
|
|
|
|
private static void ExpandAllPanels(AtomUICollapse collapse)
|
|
{
|
|
foreach (var item in GetScopeItemContainers(collapse))
|
|
{
|
|
item.IsSelected = true;
|
|
}
|
|
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
|
|
private static PixelAlignedBorder GetFrame(AtomUICollapse collapse)
|
|
{
|
|
return collapse.GetVisualDescendants()
|
|
.OfType<PixelAlignedBorder>()
|
|
.Single(static border => border.Name == "PART_Frame");
|
|
}
|
|
|
|
private static ItemsPresenter GetItemsPresenter(AtomUICollapse collapse)
|
|
{
|
|
return collapse.GetVisualDescendants()
|
|
.OfType<ItemsPresenter>()
|
|
.Single(static presenter => presenter.Name == "PART_ItemsPresenter");
|
|
}
|
|
|
|
private static StackPanel GetItemsPanel(AtomUICollapse collapse)
|
|
{
|
|
return GetItemsPresenter(collapse).Panel.ShouldBeOfType<StackPanel>();
|
|
}
|
|
|
|
private static AtomUICollapseItem[] GetScopeItemContainers(AtomUICollapse collapse)
|
|
{
|
|
return collapse.GetVisualDescendants()
|
|
.OfType<AtomUICollapseItem>()
|
|
.Where(static item => item.Classes.Contains(ScopeItemClass))
|
|
.ToArray();
|
|
}
|
|
|
|
private static PixelAlignedBorder[] GetSemanticHeaders(AtomUICollapse collapse)
|
|
{
|
|
return collapse.GetVisualDescendants()
|
|
.OfType<PixelAlignedBorder>()
|
|
.Where(static border => border.Classes.Contains(HeaderClass))
|
|
.ToArray();
|
|
}
|
|
|
|
private static IconButtonControl[] GetSemanticIcons(AtomUICollapse collapse)
|
|
{
|
|
return collapse.GetVisualDescendants()
|
|
.OfType<IconButtonControl>()
|
|
.Where(static icon => icon.Classes.Contains(IconClass))
|
|
.ToArray();
|
|
}
|
|
|
|
private static ContentPresenter[] GetSemanticTitles(AtomUICollapse collapse)
|
|
{
|
|
return collapse.GetVisualDescendants()
|
|
.OfType<ContentPresenter>()
|
|
.Where(static title => title.Classes.Contains(TitleClass))
|
|
.ToArray();
|
|
}
|
|
|
|
private static PixelAlignedBorder[] GetSemanticBodies(AtomUICollapse collapse)
|
|
{
|
|
return collapse.GetVisualDescendants()
|
|
.OfType<PixelAlignedBorder>()
|
|
.Where(static border => border.Classes.Contains(BodyClass))
|
|
.ToArray();
|
|
}
|
|
|
|
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 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 void AssertRoot(SemanticPartDescriptor part)
|
|
{
|
|
part.Path.ShouldBe("root");
|
|
part.SelectorClass.ShouldBeNull();
|
|
part.ContractType.ShouldBe(typeof(AtomUICollapse));
|
|
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");
|
|
}
|
|
|
|
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}'.");
|
|
}
|
|
}
|