mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-29 07:16:33 +08:00
444 lines
15 KiB
C#
444 lines
15 KiB
C#
using AtomUI.Controls;
|
|
using AtomUI.Controls.Primitives;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Data;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Markup.Xaml.MarkupExtensions;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using Shouldly;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using Xunit;
|
|
using AvaloniaGrid = Avalonia.Controls.Grid;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.Descriptions;
|
|
|
|
public class DescriptionsResponsiveLayoutTests
|
|
{
|
|
static DescriptionsResponsiveLayoutTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void Descriptions_Invalidates_Measure_When_Media_Breakpoint_Changes()
|
|
{
|
|
var host = new TestMediaBreakHost
|
|
{
|
|
Width = 800,
|
|
Height = 600
|
|
};
|
|
var descriptions = new AtomUI.Desktop.Controls.Descriptions
|
|
{
|
|
IsBordered = true,
|
|
ColumnInfo = ResponsiveInt.Parse("xs: 1, md: 2, lg: 3")
|
|
};
|
|
AddDescriptionItems(descriptions);
|
|
host.Children.Add(descriptions);
|
|
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 800,
|
|
Height = 600,
|
|
Content = host
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
descriptions.IsMeasureValid.ShouldBeTrue();
|
|
|
|
host.SetMediaBreakPoint(MediaBreakPoint.ExtraSmall);
|
|
|
|
descriptions.IsMeasureValid.ShouldBeFalse();
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Descriptions_ColumnInfo_Uses_Mobile_First_Cascade()
|
|
{
|
|
var descriptions = new AtomUI.Desktop.Controls.Descriptions
|
|
{
|
|
ColumnInfo = ResponsiveInt.Parse("xs: 1, md: 3")
|
|
};
|
|
|
|
descriptions.ColumnInfo!.Value.Resolve(MediaBreakPoint.Large, 9).ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public void DescriptionItem_Span_Uses_Mobile_First_Cascade()
|
|
{
|
|
var item = new DescriptionItem
|
|
{
|
|
Span = ResponsiveInt.Parse("xs: 1, md: 3")
|
|
};
|
|
|
|
item.Span.Resolve(MediaBreakPoint.ExtraExtraLarge, 1).ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public void Descriptions_Rebuilds_Item_Visuals_When_Horizontal_Bordered_State_Changes()
|
|
{
|
|
var descriptions = new AtomUI.Desktop.Controls.Descriptions();
|
|
descriptions.Items.Add(new DescriptionItem { Label = "Product", Content = "Cloud Database" });
|
|
descriptions.Items.Add(new DescriptionItem { Label = "Billing", Content = "Prepaid" });
|
|
|
|
ShowInWindow(descriptions, () =>
|
|
{
|
|
CountVisuals<DescriptionDefaultItem>(descriptions).ShouldBe(2);
|
|
|
|
descriptions.IsBordered = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
CountVisuals<DescriptionDefaultItem>(descriptions).ShouldBe(0);
|
|
CountVisuals<DescriptionBorderedItemLabel>(descriptions).ShouldBe(2);
|
|
CountVisuals<DescriptionBorderedItemContent>(descriptions).ShouldBe(2);
|
|
|
|
descriptions.IsBordered = false;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
CountVisuals<DescriptionDefaultItem>(descriptions).ShouldBe(2);
|
|
CountVisuals<DescriptionBorderedItemLabel>(descriptions).ShouldBe(0);
|
|
CountVisuals<DescriptionBorderedItemContent>(descriptions).ShouldBe(0);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Bordered_Descriptions_ContentFrame_Applies_Outer_Border()
|
|
{
|
|
var descriptions = new AtomUI.Desktop.Controls.Descriptions
|
|
{
|
|
IsBordered = true
|
|
};
|
|
descriptions.Items.Add(new DescriptionItem { Label = "Product", Content = "Cloud Database" });
|
|
|
|
ShowInWindow(descriptions, () =>
|
|
{
|
|
var contentFrame = FindVisualByName<PixelAlignedBorder>(descriptions, "ContentFrame");
|
|
contentFrame.ShouldNotBeNull();
|
|
contentFrame!.BorderBrush.ShouldNotBeNull();
|
|
contentFrame.BorderThickness.ShouldNotBe(new Thickness());
|
|
contentFrame.CornerRadius.ShouldNotBe(new CornerRadius());
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Vertical_Bordered_Default_Item_Separator_Applies_Border_Brush()
|
|
{
|
|
var descriptions = new AtomUI.Desktop.Controls.Descriptions
|
|
{
|
|
IsBordered = true,
|
|
Layout = Orientation.Vertical
|
|
};
|
|
descriptions.Items.Add(new DescriptionItem { Label = "Product", Content = "Cloud Database" });
|
|
|
|
ShowInWindow(descriptions, () =>
|
|
{
|
|
var separator = FindVisualByName<PixelAlignedBorder>(descriptions, "Separator");
|
|
separator.ShouldNotBeNull();
|
|
separator!.BorderBrush.ShouldNotBeNull();
|
|
separator.BorderThickness.ShouldBe(new Thickness(0, 1, 0, 0));
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Descriptions_Layouts_Repeated_Content_Items_By_Position()
|
|
{
|
|
var descriptions = new AtomUI.Desktop.Controls.Descriptions
|
|
{
|
|
ColumnInfo = 1
|
|
};
|
|
descriptions.Items.Add(new DescriptionItem { Label = "Same", Content = "Same content" });
|
|
descriptions.Items.Add(new DescriptionItem { Label = "Same", Content = "Same content" });
|
|
|
|
ShowInWindow(descriptions, () =>
|
|
{
|
|
var generatedItems = descriptions.GetSelfAndVisualDescendants()
|
|
.OfType<DescriptionDefaultItem>()
|
|
.ToList();
|
|
|
|
generatedItems.Count.ShouldBe(2);
|
|
AvaloniaGrid.GetRow(generatedItems[0]).ShouldBe(0);
|
|
AvaloniaGrid.GetRow(generatedItems[1]).ShouldBe(1);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DescriptionItem_Property_Changes_Update_Generated_Default_Item()
|
|
{
|
|
var item = new DescriptionItem
|
|
{
|
|
Label = "Product",
|
|
Content = "Cloud Database"
|
|
};
|
|
var descriptions = new AtomUI.Desktop.Controls.Descriptions();
|
|
descriptions.Items.Add(item);
|
|
|
|
ShowInWindow(descriptions, () =>
|
|
{
|
|
var generatedItem = descriptions.GetSelfAndVisualDescendants()
|
|
.OfType<DescriptionDefaultItem>()
|
|
.Single();
|
|
|
|
item.Label = "Billing";
|
|
item.Content = "Prepaid";
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
generatedItem.Header.ShouldBe("Billing");
|
|
generatedItem.Content.ShouldBe("Prepaid");
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DescriptionItem_Property_Changes_Update_Generated_Bordered_Items()
|
|
{
|
|
var item = new DescriptionItem
|
|
{
|
|
Label = "Product",
|
|
Content = "Cloud Database"
|
|
};
|
|
var descriptions = new AtomUI.Desktop.Controls.Descriptions
|
|
{
|
|
IsBordered = true
|
|
};
|
|
descriptions.Items.Add(item);
|
|
|
|
ShowInWindow(descriptions, () =>
|
|
{
|
|
var label = descriptions.GetSelfAndVisualDescendants()
|
|
.OfType<DescriptionBorderedItemLabel>()
|
|
.Single();
|
|
var content = descriptions.GetSelfAndVisualDescendants()
|
|
.OfType<DescriptionBorderedItemContent>()
|
|
.Single();
|
|
|
|
item.Label = "Billing";
|
|
item.Content = "Prepaid";
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
label.Content.ShouldBe("Billing");
|
|
content.Content.ShouldBe("Prepaid");
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DescriptionItem_Span_Change_Recalculates_Layout()
|
|
{
|
|
var firstItem = new DescriptionItem { Label = "A", Content = "A", Span = 1 };
|
|
var secondItem = new DescriptionItem { Label = "B", Content = "B" };
|
|
var descriptions = new AtomUI.Desktop.Controls.Descriptions
|
|
{
|
|
ColumnInfo = 2
|
|
};
|
|
descriptions.Items.Add(firstItem);
|
|
descriptions.Items.Add(secondItem);
|
|
|
|
ShowInWindow(descriptions, () =>
|
|
{
|
|
var generatedItems = descriptions.GetSelfAndVisualDescendants()
|
|
.OfType<DescriptionDefaultItem>()
|
|
.ToList();
|
|
AvaloniaGrid.GetRow(generatedItems[1]).ShouldBe(0);
|
|
|
|
firstItem.Span = 2;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
AvaloniaGrid.GetColumnSpan(generatedItems[0]).ShouldBe(2);
|
|
AvaloniaGrid.GetRow(generatedItems[1]).ShouldBe(1);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void DescriptionItem_Replacement_Detaches_Old_Item_Property_Subscriptions()
|
|
{
|
|
var oldItem = new DescriptionItem { Label = "Old", Content = "Old content" };
|
|
var newItem = new DescriptionItem { Label = "New", Content = "New content" };
|
|
var descriptions = new AtomUI.Desktop.Controls.Descriptions();
|
|
descriptions.Items.Add(oldItem);
|
|
|
|
ShowInWindow(descriptions, () =>
|
|
{
|
|
descriptions.Items = [newItem];
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
oldItem.Label = "Stale";
|
|
oldItem.Content = "Stale content";
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var generatedItem = descriptions.GetSelfAndVisualDescendants()
|
|
.OfType<DescriptionDefaultItem>()
|
|
.Single();
|
|
generatedItem.Header.ShouldBe("New");
|
|
generatedItem.Content.ShouldBe("New content");
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Dynamic_Resource_Content_Does_Not_Root_Removed_DescriptionItem()
|
|
{
|
|
var itemReference = CreateRemovedItemReference(CreateResourceKey());
|
|
|
|
CollectGarbage();
|
|
|
|
itemReference.IsAlive.ShouldBeFalse(
|
|
"DescriptionItem dynamic resources must not keep a removed item alive through Application.ResourcesChanged");
|
|
}
|
|
|
|
[Fact]
|
|
public void Dynamic_Resource_Content_Uses_Owner_Descriptions_Resources()
|
|
{
|
|
var resourceKey = CreateResourceKey();
|
|
Application.Current!.Resources[resourceKey] = "Application content";
|
|
|
|
var item = new DescriptionItem { Label = "Content" };
|
|
BindDynamicResource(item, DescriptionItem.ContentProperty, resourceKey, Application.Current);
|
|
|
|
var descriptions = new AtomUI.Desktop.Controls.Descriptions();
|
|
descriptions.Resources[resourceKey] = "Descriptions content";
|
|
descriptions.Items.Add(item);
|
|
|
|
ShowInWindow(descriptions, () =>
|
|
{
|
|
item.Content.ShouldBe("Descriptions content");
|
|
});
|
|
}
|
|
|
|
private static void AddDescriptionItems(AtomUI.Desktop.Controls.Descriptions descriptions)
|
|
{
|
|
descriptions.Items.Add(new DescriptionItem { Label = "Product", Content = "Cloud Database" });
|
|
descriptions.Items.Add(new DescriptionItem { Label = "Billing Mode", Content = "Prepaid" });
|
|
descriptions.Items.Add(new DescriptionItem { Label = "Automatic Renewal", Content = "YES" });
|
|
descriptions.Items.Add(new DescriptionItem { Label = "Order Time", Content = "2018-04-24 18:00:00" });
|
|
descriptions.Items.Add(new DescriptionItem { Label = "Usage Time", Content = "2019-04-24 18:00:00", Span = 2 });
|
|
descriptions.Items.Add(new DescriptionItem { Label = "Status", Content = "Running", Span = 3 });
|
|
}
|
|
|
|
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 int CountVisuals<T>(Control root)
|
|
where T : Control
|
|
{
|
|
return root.GetSelfAndVisualDescendants().OfType<T>().Count();
|
|
}
|
|
|
|
private static T? FindVisualByName<T>(Control root, string name)
|
|
where T : Control
|
|
{
|
|
return root.GetSelfAndVisualDescendants()
|
|
.OfType<T>()
|
|
.FirstOrDefault(control => control.Name == name);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private static WeakReference CreateRemovedItemReference(string resourceKey)
|
|
{
|
|
Application.Current!.Resources[resourceKey] = "Application content";
|
|
|
|
var item = new DescriptionItem { Label = "Content" };
|
|
BindDynamicResource(item, DescriptionItem.ContentProperty, resourceKey, Application.Current);
|
|
|
|
var descriptions = new AtomUI.Desktop.Controls.Descriptions();
|
|
descriptions.Items.Add(item);
|
|
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 800,
|
|
Height = 600,
|
|
Content = descriptions
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
item.Content.ShouldBe("Application content");
|
|
descriptions.Items.Remove(item);
|
|
Dispatcher.UIThread.RunJobs();
|
|
return new WeakReference(item);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
private static string CreateResourceKey()
|
|
{
|
|
return $"DescriptionsResponsiveLayoutTests.Content.{Guid.NewGuid():N}";
|
|
}
|
|
|
|
private static void BindDynamicResource(AvaloniaObject target, AvaloniaProperty property, object key, object? anchor)
|
|
{
|
|
const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;
|
|
var bindMethod = typeof(AvaloniaObject).GetMethod(
|
|
"Bind",
|
|
flags,
|
|
binder: null,
|
|
types: [typeof(AvaloniaProperty), typeof(BindingBase), typeof(object)],
|
|
modifiers: null);
|
|
|
|
bindMethod.ShouldNotBeNull();
|
|
|
|
var extension = new DynamicResourceExtension(key);
|
|
var anchorField = typeof(DynamicResourceExtension).GetField("_anchor", flags);
|
|
anchorField.ShouldNotBeNull();
|
|
anchorField.SetValue(extension, anchor);
|
|
|
|
bindMethod.Invoke(target, [property, extension, anchor]);
|
|
}
|
|
|
|
private static void CollectGarbage()
|
|
{
|
|
for (var i = 0; i < 3; i++)
|
|
{
|
|
GC.Collect();
|
|
GC.WaitForPendingFinalizers();
|
|
GC.Collect();
|
|
}
|
|
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
|
|
private sealed class TestMediaBreakHost : Panel, IMediaBreakAwareControl
|
|
{
|
|
public MediaBreakPoint MediaBreakPoint { get; private set; } = MediaBreakPoint.Large;
|
|
|
|
public event EventHandler<MediaBreakPointChangedEventArgs>? MediaBreakPointChanged;
|
|
|
|
public void SetMediaBreakPoint(MediaBreakPoint mediaBreakPoint)
|
|
{
|
|
MediaBreakPoint = mediaBreakPoint;
|
|
MediaBreakPointChanged?.Invoke(this, new MediaBreakPointChangedEventArgs(mediaBreakPoint));
|
|
}
|
|
}
|
|
}
|