Files

548 lines
20 KiB
C#

using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Headless;
using Avalonia.Layout;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Shouldly;
using Xunit;
using AtomUISizeType = AtomUI.SizeType;
using AvaloniaTextBlock = Avalonia.Controls.TextBlock;
using AvaloniaWindow = Avalonia.Controls.Window;
namespace AtomUI.Desktop.Controls.Tests.Steps;
public class StepsLayoutMatrixTests
{
static StepsLayoutMatrixTests()
{
AvaloniaTestApp.EnsureInitialized();
}
public static IEnumerable<object[]> LayoutCases
{
get
{
foreach (var type in Enum.GetValues<Desktop.Controls.StepsType>())
foreach (var orientation in Enum.GetValues<Orientation>())
foreach (var titlePlacement in Enum.GetValues<Orientation>())
foreach (var sizeType in new[] { AtomUISizeType.Middle, AtomUISizeType.Small })
{
yield return [type, orientation, titlePlacement, sizeType];
}
}
}
[Theory]
[MemberData(nameof(LayoutCases))]
public void Every_Layout_Combination_Uses_One_Finite_Semantic_Tree(
Desktop.Controls.StepsType type,
Orientation orientation,
Orientation titlePlacement,
AtomUISizeType sizeType)
{
var steps = CreateSteps(type, orientation, titlePlacement, sizeType);
ShowInWindow(steps, () =>
{
foreach (var item in steps.Items.Cast<Desktop.Controls.StepsItem>())
{
var indicator = item.GetVisualDescendants()
.OfType<Desktop.Controls.StepsItemIndicator>()
.Single();
var header = FindControl(item, "HeaderPresenter");
var subHeader = FindControl(item, "SubHeaderPresenter");
var content = FindControl(item, "ContentPresenter");
var connector = FindControl(item, "Connector");
var arrow = FindControl(item, "NavigationArrow");
var panelArrow = FindControl(item, "PanelArrow");
AssertFinite(indicator);
AssertFinite(header);
AssertFinite(subHeader);
AssertFinite(content);
AssertFinite(connector);
AssertFinite(arrow);
AssertFinite(panelArrow);
indicator.IsVisible.ShouldBe(type != Desktop.Controls.StepsType.Panel);
if (type != Desktop.Controls.StepsType.Panel)
{
indicator.Bounds.Width.ShouldBeGreaterThan(0);
indicator.Bounds.Height.ShouldBeGreaterThan(0);
}
AssertNoInteriorOverlap(indicator, header);
AssertNoInteriorOverlap(indicator, subHeader);
AssertNoInteriorOverlap(indicator, content);
AssertNoInteriorOverlap(header, content);
AssertNoInteriorOverlap(subHeader, content);
content.IsVisible.ShouldBe(type != Desktop.Controls.StepsType.Inline);
connector.IsVisible.ShouldBe(
type != Desktop.Controls.StepsType.Navigation &&
type != Desktop.Controls.StepsType.Panel &&
!item.IsLast);
arrow.IsVisible.ShouldBe(type == Desktop.Controls.StepsType.Navigation && !item.IsLast);
panelArrow.IsVisible.ShouldBe(type == Desktop.Controls.StepsType.Panel && !item.IsLast);
}
});
}
[Fact]
public void Runtime_Type_Orientation_TitlePlacement_And_Size_Changes_Rearrange_The_Same_Nodes()
{
var steps = CreateSteps(
Desktop.Controls.StepsType.Default,
Orientation.Horizontal,
Orientation.Horizontal,
AtomUISizeType.Middle);
ShowInWindow(steps, () =>
{
var item = steps.Items[0].ShouldBeOfType<Desktop.Controls.StepsItem>();
var indicator = item.GetVisualDescendants()
.OfType<Desktop.Controls.StepsItemIndicator>()
.Single();
var header = FindControl(item, "HeaderPresenter");
var connector = FindControl(item, "Connector");
var arrow = FindControl(item, "NavigationArrow");
var middleWidth = indicator.Bounds.Width;
header.Bounds.X.ShouldBeGreaterThanOrEqualTo(indicator.Bounds.Right);
steps.Type = Desktop.Controls.StepsType.Dot;
Relayout();
header.Bounds.Y.ShouldBeGreaterThanOrEqualTo(indicator.Bounds.Bottom);
steps.Type = Desktop.Controls.StepsType.Navigation;
steps.Orientation = Orientation.Vertical;
steps.TitlePlacement = Orientation.Vertical;
Relayout();
connector.IsVisible.ShouldBeFalse();
arrow.IsVisible.ShouldBeTrue();
arrow.Bounds.Y.ShouldBeGreaterThanOrEqualTo(header.Bounds.Bottom);
steps.Type = Desktop.Controls.StepsType.Default;
steps.Orientation = Orientation.Horizontal;
steps.TitlePlacement = Orientation.Horizontal;
steps.SizeType = AtomUISizeType.Small;
Relayout();
indicator.Bounds.Width.ShouldBeLessThan(middleWidth);
});
}
[Theory]
[InlineData(Desktop.Controls.StepsType.Dot)]
[InlineData(Desktop.Controls.StepsType.OutlineDot)]
public void Dot_Types_Reserve_Current_Size_And_Inline_Content_Is_Removed_From_Layout(
Desktop.Controls.StepsType type)
{
var steps = CreateSteps(
type,
Orientation.Horizontal,
Orientation.Horizontal,
AtomUISizeType.Middle);
ShowInWindow(steps, () =>
{
var first = steps.Items[0].ShouldBeOfType<Desktop.Controls.StepsItem>();
var second = steps.Items[1].ShouldBeOfType<Desktop.Controls.StepsItem>();
var currentIndicator = GetIndicator(first);
var waitingIndicator = GetIndicator(second);
var currentFrame = GetIndicatorFrame(currentIndicator);
var waitingFrame = GetIndicatorFrame(waitingIndicator);
currentIndicator.Bounds.Width.ShouldBe(waitingIndicator.Bounds.Width);
currentIndicator.Bounds.Height.ShouldBe(waitingIndicator.Bounds.Height);
currentFrame.Bounds.Width.ShouldBeGreaterThan(waitingFrame.Bounds.Width);
currentFrame.Bounds.Height.ShouldBeGreaterThan(waitingFrame.Bounds.Height);
steps.Type = Desktop.Controls.StepsType.Inline;
Relayout();
FindControl(first, "ContentPresenter").IsVisible.ShouldBeFalse();
GetIndicator(first).Bounds.Width.ShouldBe(GetIndicator(second).Bounds.Width);
});
}
[Theory]
[InlineData(AtomUISizeType.Middle)]
[InlineData(AtomUISizeType.Small)]
public void Horizontal_Default_Connector_Has_Symmetric_Endpoint_Gaps(AtomUISizeType sizeType)
{
var steps = CreateSteps(
Desktop.Controls.StepsType.Default,
Orientation.Horizontal,
Orientation.Horizontal,
sizeType);
steps.Items[0].ShouldBeOfType<Desktop.Controls.StepsItem>().Content =
"A description that is deliberately wider than the title row";
ShowInWindow(steps, () =>
{
var first = steps.Items[0].ShouldBeOfType<Desktop.Controls.StepsItem>();
var layoutPanel = GetLayoutPanel(first);
var header = FindControl(first, "HeaderPresenter");
var subHeader = FindControl(first, "SubHeaderPresenter");
var connector = FindControl(first, "Connector");
var headerRowRight = Math.Max(header.Bounds.Right, subHeader.Bounds.Right);
connector.Margin.Left.ShouldBeGreaterThan(0);
connector.Margin.Right.ShouldBe(connector.Margin.Left);
connector.Bounds.X.ShouldBe(headerRowRight + connector.Margin.Left, 0.01);
(layoutPanel.Bounds.Width - connector.Bounds.Right).ShouldBe(connector.Margin.Right, 0.01);
});
}
[Theory]
[InlineData(AtomUISizeType.Middle)]
[InlineData(AtomUISizeType.Small)]
public void Horizontal_Default_Connector_Center_Matches_Indicator_Center(AtomUISizeType sizeType)
{
var steps = CreateSteps(
Desktop.Controls.StepsType.Default,
Orientation.Horizontal,
Orientation.Horizontal,
sizeType);
ShowInWindow(steps, () =>
{
foreach (var item in steps.Items.Cast<Desktop.Controls.StepsItem>().Take(2))
{
var indicator = GetIndicator(item);
var connector = FindControl(item, "Connector");
connector.Bounds.Center.Y.ShouldBe(indicator.Bounds.Center.Y, 0.01);
}
});
}
[Theory]
[InlineData(AtomUISizeType.Middle)]
[InlineData(AtomUISizeType.Small)]
public void Horizontal_Default_Header_Row_Centers_Stay_Aligned_When_SubHeader_Is_Present(AtomUISizeType sizeType)
{
var steps = CreateSteps(
Desktop.Controls.StepsType.Default,
Orientation.Horizontal,
Orientation.Horizontal,
sizeType);
ShowInWindow(steps, () =>
{
var items = steps.Items.Cast<Desktop.Controls.StepsItem>().ToArray();
var centerY = GetIndicator(items[0]).Bounds.Center.Y;
foreach (var item in items)
{
GetIndicator(item).Bounds.Center.Y.ShouldBe(centerY, 0.01);
FindControl(item, "HeaderPresenter").Bounds.Center.Y.ShouldBe(centerY, 0.01);
}
FindControl(items[1], "SubHeaderPresenter").Bounds.Center.Y.ShouldBe(centerY, 0.01);
foreach (var item in items.Take(2))
{
FindControl(item, "Connector").Bounds.Center.Y.ShouldBe(centerY, 0.01);
}
});
}
[Theory]
[InlineData(AtomUISizeType.Middle)]
[InlineData(AtomUISizeType.Small)]
public void Horizontal_Default_Content_Row_Starts_At_Same_Y_When_SubHeader_Is_Present(AtomUISizeType sizeType)
{
var steps = CreateSteps(
Desktop.Controls.StepsType.Default,
Orientation.Horizontal,
Orientation.Horizontal,
sizeType);
ShowInWindow(steps, () =>
{
var items = steps.Items.Cast<Desktop.Controls.StepsItem>().ToArray();
var contentY = FindControl(items[0], "ContentPresenter").Bounds.Y;
foreach (var item in items)
{
FindControl(item, "ContentPresenter").Bounds.Y.ShouldBe(contentY, 0.01);
}
});
}
[Theory]
[InlineData(AtomUISizeType.Middle)]
[InlineData(AtomUISizeType.Small)]
public void Horizontal_Default_SubHeader_Margin_Is_Inline_Start_Only(AtomUISizeType sizeType)
{
var steps = CreateSteps(
Desktop.Controls.StepsType.Default,
Orientation.Horizontal,
Orientation.Horizontal,
sizeType);
ShowInWindow(steps, () =>
{
var second = steps.Items[1].ShouldBeOfType<Desktop.Controls.StepsItem>();
var header = FindControl(second, "HeaderPresenter");
var subHeader = FindControl(second, "SubHeaderPresenter");
subHeader.IsVisible.ShouldBeTrue();
subHeader.Margin.Left.ShouldBeGreaterThan(0);
subHeader.Margin.Top.ShouldBe(0);
subHeader.Margin.Right.ShouldBe(0);
subHeader.Margin.Bottom.ShouldBe(0);
subHeader.Bounds.Center.Y.ShouldBe(header.Bounds.Center.Y, 0.01);
});
}
[Theory]
[InlineData(AtomUISizeType.Middle)]
[InlineData(AtomUISizeType.Small)]
public void Horizontal_Default_Items_Without_SubHeader_Do_Not_Reserve_SubHeader_Margin(AtomUISizeType sizeType)
{
var steps = CreateSteps(
Desktop.Controls.StepsType.Default,
Orientation.Horizontal,
Orientation.Horizontal,
sizeType);
steps.Items[0].ShouldBeOfType<Desktop.Controls.StepsItem>().SubHeader = null;
steps.Items[2].ShouldBeOfType<Desktop.Controls.StepsItem>().SubHeader = null;
ShowInWindow(steps, () =>
{
var items = steps.Items.Cast<Desktop.Controls.StepsItem>().ToArray();
var rowCenterY = GetIndicator(items[0]).Bounds.Center.Y;
var contentY = FindControl(items[0], "ContentPresenter").Bounds.Y;
FindControl(items[0], "SubHeaderPresenter").IsVisible.ShouldBeFalse();
FindControl(items[2], "SubHeaderPresenter").IsVisible.ShouldBeFalse();
foreach (var item in items)
{
GetIndicator(item).Bounds.Center.Y.ShouldBe(rowCenterY, 0.01);
FindControl(item, "HeaderPresenter").Bounds.Center.Y.ShouldBe(rowCenterY, 0.01);
FindControl(item, "ContentPresenter").Bounds.Y.ShouldBe(contentY, 0.01);
}
var firstHeader = FindControl(items[0], "HeaderPresenter");
var firstConnector = FindControl(items[0], "Connector");
firstConnector.Bounds.X.ShouldBe(firstHeader.Bounds.Right + firstConnector.Margin.Left, 0.01);
});
}
[Theory]
[InlineData(AtomUISizeType.Middle)]
[InlineData(AtomUISizeType.Small)]
public void Vertical_Default_Content_Text_Starts_At_Header_Text_Start_When_SubHeader_Is_Present(AtomUISizeType sizeType)
{
var steps = CreateSteps(
Desktop.Controls.StepsType.Default,
Orientation.Vertical,
Orientation.Horizontal,
sizeType);
ShowInWindow(steps, () =>
{
var second = steps.Items[1].ShouldBeOfType<Desktop.Controls.StepsItem>();
var header = FindControl(second, "HeaderPresenter").ShouldBeOfType<ContentPresenter>();
var content = FindControl(second, "ContentPresenter").ShouldBeOfType<ContentPresenter>();
var headerText = header.Content.ShouldBeOfType<AvaloniaTextBlock>();
var contentText = content.Content.ShouldBeOfType<AvaloniaTextBlock>();
content.Margin.Left.ShouldBe(0);
content.Padding.Left.ShouldBe(0);
GetLeftInItem(headerText, second).ShouldBe(GetLeftInItem(contentText, second), 0.01);
});
}
[Fact]
public void Horizontal_Default_Item_Row_Is_Centered_When_Steps_Is_Stretched()
{
var steps = CreateSteps(
Desktop.Controls.StepsType.Default,
Orientation.Horizontal,
Orientation.Horizontal,
AtomUISizeType.Middle);
ShowInWindow(steps, () =>
{
var first = steps.Items[0].ShouldBeOfType<Desktop.Controls.StepsItem>();
first.Bounds.Y.ShouldBeGreaterThan(0);
first.Bounds.Center.Y.ShouldBe(steps.Bounds.Center.Y, 0.01);
});
}
[Fact]
public void Default_Indicator_Displays_The_OneBased_Step_Number()
{
var steps = CreateSteps(
Desktop.Controls.StepsType.Default,
Orientation.Horizontal,
Orientation.Horizontal,
AtomUISizeType.Middle);
steps.Initial = 4;
steps.Current = 4;
ShowInWindow(steps, () =>
{
var first = steps.Items[0].ShouldBeOfType<Desktop.Controls.StepsItem>();
var number = GetIndicator(first).GetVisualDescendants()
.OfType<Avalonia.Controls.TextBlock>()
.Single(control => control.Name == "StepNumberText");
number.Text.ShouldBe("5");
});
}
[Fact]
public void Offset_Is_Bound_To_Items_Panel()
{
var steps = CreateSteps(
Desktop.Controls.StepsType.Inline,
Orientation.Horizontal,
Orientation.Vertical,
AtomUISizeType.Middle);
steps.Offset = 2;
ShowInWindow(steps, () =>
{
var panel = steps.GetVisualDescendants().OfType<Desktop.Controls.StepsPanel>().Single();
panel.Offset.ShouldBe(2);
});
}
[Fact]
public void Horizontal_Content_Alignment_Is_Bound_To_Items_Panel()
{
var steps = CreateSteps(
Desktop.Controls.StepsType.Navigation,
Orientation.Vertical,
Orientation.Horizontal,
AtomUISizeType.Middle);
steps.HorizontalContentAlignment = HorizontalAlignment.Right;
ShowInWindow(steps, () =>
{
var panel = steps.GetVisualDescendants().OfType<Desktop.Controls.StepsPanel>().Single();
panel.HorizontalContentAlignment.ShouldBe(HorizontalAlignment.Right);
});
}
private static Desktop.Controls.Steps CreateSteps(
Desktop.Controls.StepsType type,
Orientation orientation,
Orientation titlePlacement,
AtomUISizeType sizeType)
{
var steps = new Desktop.Controls.Steps
{
Width = 720,
Current = 0,
Type = type,
Orientation = orientation,
TitlePlacement = titlePlacement,
SizeType = sizeType,
IsItemClickable = true
};
steps.Items.Add(new Desktop.Controls.StepsItem
{
Header = "First step",
SubHeader = "00:01",
Content = "First step content"
});
steps.Items.Add(new Desktop.Controls.StepsItem
{
Header = "Second step",
SubHeader = "00:02",
Content = "Second step content"
});
steps.Items.Add(new Desktop.Controls.StepsItem
{
Header = "Last step",
SubHeader = "00:03",
Content = "Last step content"
});
return steps;
}
private static Desktop.Controls.StepsItemIndicator GetIndicator(Desktop.Controls.StepsItem item)
{
return item.GetVisualDescendants().OfType<Desktop.Controls.StepsItemIndicator>().Single();
}
private static Desktop.Controls.StepsItemLayoutPanel GetLayoutPanel(Desktop.Controls.StepsItem item)
{
return item.GetVisualDescendants().OfType<Desktop.Controls.StepsItemLayoutPanel>().Single();
}
private static Control GetIndicatorFrame(Desktop.Controls.StepsItemIndicator indicator)
{
return indicator.GetVisualDescendants().OfType<Control>().Single(control => control.Name == "Frame");
}
private static Control FindControl(Desktop.Controls.StepsItem item, string name)
{
return item.GetVisualDescendants().OfType<Control>().Single(control => control.Name == name);
}
private static double GetLeftInItem(Control control, Desktop.Controls.StepsItem item)
{
return control.TranslatePoint(default, item)!.Value.X;
}
private static void AssertFinite(Control control)
{
double.IsFinite(control.Bounds.X).ShouldBeTrue();
double.IsFinite(control.Bounds.Y).ShouldBeTrue();
double.IsFinite(control.Bounds.Width).ShouldBeTrue();
double.IsFinite(control.Bounds.Height).ShouldBeTrue();
control.Bounds.Width.ShouldBeGreaterThanOrEqualTo(0);
control.Bounds.Height.ShouldBeGreaterThanOrEqualTo(0);
}
private static void AssertNoInteriorOverlap(Control first, Control second)
{
if (!first.IsVisible || !second.IsVisible)
{
return;
}
var intersection = first.Bounds.Intersect(second.Bounds);
(intersection.Width > 0 && intersection.Height > 0).ShouldBeFalse(
$"{first.Name ?? first.GetType().Name} overlaps {second.Name ?? second.GetType().Name}.");
}
private static void Relayout()
{
Dispatcher.UIThread.RunJobs();
AvaloniaHeadlessPlatform.ForceRenderTimerTick(1);
Dispatcher.UIThread.RunJobs();
}
private static void ShowInWindow(Control content, Action assertion)
{
var window = new AvaloniaWindow
{
Width = 900,
Height = 420,
Content = content
};
try
{
window.Show();
Relayout();
assertion();
}
finally
{
window.Close();
}
}
}