diff --git a/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs b/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs index 78268cf..b3769ef 100644 --- a/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs +++ b/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs @@ -421,7 +421,26 @@ public abstract class NumericUpDownBase : NumericUpDown where T : struct, ICo #pragma warning disable AVP1002 public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register, T?>( - nameof(Value), defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); + nameof(Value), defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true, coerce: CoerceCurrentValue); + + private static T? CoerceCurrentValue(AvaloniaObject instance, T? arg2) + { + if (instance is not NumericUpDownBase { IsInitialized: true } n) return arg2; + if (arg2 is null) + { + return n.EmptyInputValue; + } + var value = arg2.Value; + if(value.CompareTo(n.Minimum) < 0) + { + return n.Minimum; + } + if (value.CompareTo(n.Maximum) > 0) + { + return n.Maximum; + } + return arg2.Value; + } public T? Value { @@ -760,6 +779,7 @@ public abstract class NumericUpDownBase : NumericUpDown where T : struct, ICo protected override void Increase() { + if (IsReadOnly) return; T? value; if (Value is not null) { diff --git a/tests/HeadlessTest.Ursa/Controls/AnchorTests/Tests.cs b/tests/HeadlessTest.Ursa/Controls/AnchorTests/Tests.cs index 2e57d4a..82fecfc 100644 --- a/tests/HeadlessTest.Ursa/Controls/AnchorTests/Tests.cs +++ b/tests/HeadlessTest.Ursa/Controls/AnchorTests/Tests.cs @@ -92,6 +92,184 @@ public class Tests var items = window.GetVisualDescendants().OfType().ToList(); Assert.NotEmpty(items); Assert.Equal(10, items.Count); + } + [AvaloniaFact] + public void TopOffset_Property_Should_Affect_Scroll_Position() + { + var window = new Window() + { + Width = 500, + Height = 500, + }; + var view = new TestView(); + window.Content = view; + window.Show(); + + var anchor = view.FindControl("Anchor"); + var scrollViewer = view.FindControl("ScrollViewer"); + + Assert.NotNull(anchor); + Assert.NotNull(scrollViewer); + + // Set TopOffset to 50 + anchor.TopOffset = 50; + + // Scroll to position that should trigger item2 selection + scrollViewer.Offset = new Vector(0, 310.0); + Dispatcher.UIThread.RunJobs(); + + // Check that the offset affects position calculations + Assert.Equal(50, anchor.TopOffset); + + // The behavior should account for the top offset + anchor.InvalidatePositions(); + Dispatcher.UIThread.RunJobs(); + } + + [AvaloniaFact] + public void InvalidatePositions_Should_Update_Internal_Positions() + { + var window = new Window() + { + Width = 500, + Height = 500, + }; + var view = new TestView(); + window.Content = view; + window.Show(); + + var anchor = view.FindControl("Anchor"); + var scrollViewer = view.FindControl("ScrollViewer"); + + Assert.NotNull(anchor); + Assert.NotNull(scrollViewer); + + Dispatcher.UIThread.RunJobs(); + + // Call InvalidatePositions explicitly + anchor.InvalidatePositions(); + Dispatcher.UIThread.RunJobs(); + + // Verify that positions are correctly calculated by checking selection + var item1 = view.FindControl("Item1"); + Assert.NotNull(item1); + Assert.True(item1.IsSelected); // Should be selected at top + } + + [AvaloniaFact] + public void Anchor_Id_Attached_Property_Should_Work() + { + var border = new Border(); + + // Test SetId and GetId + Anchor.SetId(border, "test-id"); + var retrievedId = Anchor.GetId(border); + + Assert.Equal("test-id", retrievedId); + + // Test with null + Anchor.SetId(border, null); + var nullId = Anchor.GetId(border); + Assert.Null(nullId); + } + + [AvaloniaFact] + public void Anchor_Without_TargetContainer_Should_Not_Crash() + { + var window = new Window(); + var anchor = new Anchor(); + window.Content = anchor; + window.Show(); + + // These operations should not crash when TargetContainer is null + anchor.InvalidatePositions(); + Dispatcher.UIThread.RunJobs(); + + // Should not throw + Assert.Null(anchor.TargetContainer); + } + + [AvaloniaFact] + public void AnchorItem_Level_Property_Should_Calculate_Correctly() + { + var window = new Window() + { + Width = 500, + Height = 500, + }; + var view = new TestView(); + window.Content = view; + window.Show(); + + Dispatcher.UIThread.RunJobs(); + + var item1 = view.FindControl("Item1"); + var item2 = view.FindControl("Item2"); + var item4 = view.FindControl("Item4"); + + Assert.NotNull(item1); + Assert.NotNull(item2); + Assert.NotNull(item4); + + // Based on the XAML structure, Item1 is inside Anchor (level 1) + Assert.Equal(1, item1.Level); + + // Item2 is nested inside Item1, so level 2 + Assert.Equal(2, item2.Level); + + // Item4 is at the same level as Item1 + Assert.Equal(1, item4.Level); + } + + [AvaloniaFact] + public void AnchorItem_Without_Anchor_Parent_Should_Throw() + { + // This test verifies that AnchorItem throws when not inside an Anchor + var anchorItem = new AnchorItem(); + var window = new Window(); + + // Add some items to the AnchorItem to trigger container creation + anchorItem.ItemsSource = new[] { "Item1", "Item2" }; + window.Content = anchorItem; + + // The exception should be thrown when showing the window + var exception = Assert.Throws(() => window.Show()); + Assert.Contains("AnchorItem must be inside an Anchor control", exception.Message); + } + + [AvaloniaFact] + public async void Scroll_To_Bottom_Should_Handle_Edge_Case() + { + var window = new Window() + { + Width = 500, + Height = 500, + }; + var view = new TestView(); + window.Content = view; + window.Show(); + + var anchor = view.FindControl("Anchor"); + var scrollViewer = view.FindControl("ScrollViewer"); + + Assert.NotNull(anchor); + Assert.NotNull(scrollViewer); + + Dispatcher.UIThread.RunJobs(); + + // Scroll to the very bottom + var maxOffset = scrollViewer.Extent.Height - scrollViewer.Bounds.Height; + scrollViewer.Offset = new Vector(0, maxOffset); + Dispatcher.UIThread.RunJobs(); + + // Should handle the edge case without crashing + anchor.InvalidatePositions(); + Dispatcher.UIThread.RunJobs(); + + // The last item should be selected + var lastItems = window.GetVisualDescendants().OfType() + .Where(i => i.IsSelected).ToList(); + Assert.Single(lastItems); } } \ No newline at end of file diff --git a/tests/HeadlessTest.Ursa/Controls/ButtonGroupTests/ButtonGroupTests.cs b/tests/HeadlessTest.Ursa/Controls/ButtonGroupTests/ButtonGroupTests.cs new file mode 100644 index 0000000..023afca --- /dev/null +++ b/tests/HeadlessTest.Ursa/Controls/ButtonGroupTests/ButtonGroupTests.cs @@ -0,0 +1,225 @@ +using System.Collections.ObjectModel; +using System.Windows.Input; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Headless.XUnit; +using Avalonia.LogicalTree; +using Avalonia.VisualTree; +using CommunityToolkit.Mvvm.Input; +using Ursa.Controls; + +namespace HeadlessTest.Ursa.Controls.ButtonGroupTests; + +public class ButtonGroupTests +{ + [AvaloniaFact] + public void ButtonGroup_Should_Create_Button_Containers_For_Non_Button_Items() + { + // Arrange + var window = new Window(); + var buttonGroup = new ButtonGroup(); + window.Content = buttonGroup; + window.Show(); + + var items = new ObservableCollection { "Item1", "Item2", "Item3" }; + buttonGroup.ItemsSource = items; + + // Act & Assert + var generatedButtons = buttonGroup.GetVisualDescendants().OfType