// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using FluentAssertions; using Microsoft.Maui.Graphics; using Microsoft.Maui.Platform; using Xunit; namespace Microsoft.Maui.Controls.Linux.Tests.Views; /// /// Edge case and boundary condition tests for SkiaView. /// Uses concrete SkiaLabel/SkiaButton since SkiaView is abstract. /// public class SkiaViewEdgeCaseTests { [Fact] public void Opacity_ClampedToValidRange() { // Arrange var view = new SkiaLabel(); // Act - set above max view.Opacity = 1.5f; // Assert - clamped to 1.0 via coerceValue view.Opacity.Should().Be(1.0f); // Act - set below min view.Opacity = -0.5f; // Assert - clamped to 0.0 view.Opacity.Should().Be(0.0f); } [Fact] public void Bounds_DefaultIsZero() { // Arrange & Act var view = new SkiaLabel(); // Assert view.Bounds.Should().Be(new Rect(0, 0, 0, 0)); } [Fact] public void IsVisible_WhenFalse_HitTestReturnsNull() { // Arrange var view = new SkiaButton(); view.Bounds = new Rect(0, 0, 100, 100); view.IsVisible = false; // Act - hit test at center of bounds var result = view.HitTest(50, 50); // Assert result.Should().BeNull(); } [Fact] public void Children_DefaultEmpty() { // Arrange & Act var view = new SkiaLabel(); // Assert view.Children.Should().BeEmpty(); } [Fact] public void Measure_WithZeroSize_ReturnsZero() { // Arrange var view = new SkiaLabel(); // Act var result = view.Measure(new Size(0, 0)); // Assert - should not throw, and result should have non-negative dimensions result.Width.Should().BeGreaterThanOrEqualTo(0); result.Height.Should().BeGreaterThanOrEqualTo(0); } [Fact] public void Arrange_WithNegativeValues_DoesNotThrow() { // Arrange var view = new SkiaLabel(); // Act & Assert - should not throw var exception = Record.Exception(() => view.Arrange(new Rect(-10, -20, -50, -100))); exception.Should().BeNull(); } [Fact] public void Scale_DefaultIsOne() { // Arrange & Act var view = new SkiaLabel(); // Assert view.Scale.Should().Be(1.0); } [Fact] public void Rotation_DefaultIsZero() { // Arrange & Act var view = new SkiaLabel(); // Assert view.Rotation.Should().Be(0.0); } [Fact] public void Margin_DefaultIsZero() { // Arrange & Act var view = new SkiaLabel(); // Assert view.Margin.Should().Be(default(Thickness)); } [Fact] public void Padding_DefaultIsZero() { // Arrange & Act - Use SkiaLabel which doesn't override default padding var view = new SkiaLabel(); // Assert view.Padding.Should().Be(default(Thickness)); } [Fact] public void Dispose_CanBeCalledMultipleTimes() { // Arrange var view = new SkiaLabel(); // Act & Assert - calling Dispose twice should not throw var exception = Record.Exception(() => { view.Dispose(); view.Dispose(); }); exception.Should().BeNull(); } [Fact] public void InputTransparent_WhenTrue_HitTestReturnsNull() { // Arrange var view = new SkiaButton(); view.Bounds = new Rect(0, 0, 100, 100); view.InputTransparent = true; // Act - hit test at center of bounds var result = view.HitTest(50, 50); // Assert - InputTransparent causes HitTest to return null result.Should().BeNull(); } } /// /// Edge case tests for SkiaLabel. /// public class SkiaLabelEdgeCaseTests { [Fact] public void Text_Null_TreatedAsEmpty() { // Arrange var label = new SkiaLabel(); // Act & Assert - setting null should not throw var exception = Record.Exception(() => label.Text = null!); exception.Should().BeNull(); // Text may be null or empty depending on BindableProperty behavior (label.Text == null || label.Text == string.Empty).Should().BeTrue(); } [Fact] public void FontSize_Zero_DoesNotThrow() { // Arrange var label = new SkiaLabel(); // Act & Assert var exception = Record.Exception(() => label.FontSize = 0); exception.Should().BeNull(); } [Fact] public void FontSize_Negative_DoesNotThrow() { // Arrange var label = new SkiaLabel(); // Act & Assert var exception = Record.Exception(() => label.FontSize = -1); exception.Should().BeNull(); } [Fact] public void MaxLines_Negative_DoesNotThrow() { // Arrange var label = new SkiaLabel(); // Act & Assert var exception = Record.Exception(() => label.MaxLines = -1); exception.Should().BeNull(); } [Fact] public void CharacterSpacing_Negative_DoesNotThrow() { // Arrange var label = new SkiaLabel(); // Act & Assert var exception = Record.Exception(() => label.CharacterSpacing = -5.0); exception.Should().BeNull(); } [Fact] public void Measure_EmptyText_ReturnsSmallSize() { // Arrange var label = new SkiaLabel(); label.Text = string.Empty; // Act var size = label.Measure(new Size(500, 500)); // Assert - should return non-negative size (padding + font height minimum) size.Width.Should().BeGreaterThanOrEqualTo(0); size.Height.Should().BeGreaterThanOrEqualTo(0); } [Fact] public void LineBreakMode_AllValues_DoNotThrow() { // Arrange var label = new SkiaLabel(); // Act & Assert - iterate all LineBreakMode values foreach (var mode in Enum.GetValues()) { var exception = Record.Exception(() => label.LineBreakMode = mode); exception.Should().BeNull($"setting LineBreakMode to {mode} should not throw"); } } } /// /// Edge case tests for SkiaEntry. /// public class SkiaEntryEdgeCaseTests { [Fact] public void Text_WhenReadOnly_CanStillBeSetProgrammatically() { // Arrange var entry = new SkiaEntry(); entry.IsReadOnly = true; // Act - programmatic set should still work (IsReadOnly only blocks user input) entry.Text = "hello"; // Assert entry.Text.Should().Be("hello"); } [Fact] public void MaxLength_WhenSet_TruncatesExistingText() { // Arrange var entry = new SkiaEntry(); entry.Text = "Hello World"; // Act - MaxLength is a constraint for input, not retroactive truncation entry.MaxLength = 5; // Assert - MaxLength property is set; Text may or may not be truncated // depending on implementation. The property itself should be set. entry.MaxLength.Should().Be(5); // Note: In this implementation, MaxLength only constrains new input, // it does not retroactively truncate existing text. } [Fact] public void CursorPosition_BeyondTextLength_ClampedOrSafe() { // Arrange var entry = new SkiaEntry(); entry.Text = "Hi"; // Act & Assert - CursorPosition setter uses Math.Clamp(value, 0, Text.Length) var exception = Record.Exception(() => entry.CursorPosition = 100); exception.Should().BeNull(); // Should be clamped to text length entry.CursorPosition.Should().BeLessThanOrEqualTo(entry.Text.Length); } [Fact] public void SelectionLength_BeyondTextLength_ClampedOrSafe() { // Arrange var entry = new SkiaEntry(); entry.Text = "Hi"; // Act & Assert - should not throw var exception = Record.Exception(() => entry.SelectionLength = 100); exception.Should().BeNull(); } [Fact] public void TextChanged_NotRaisedWhenSameValue() { // Arrange var entry = new SkiaEntry(); entry.Text = "A"; int eventCount = 0; entry.TextChanged += (s, e) => eventCount++; // Act - set same value again entry.Text = "A"; // Assert - BindableProperty does not raise propertyChanged when value is the same eventCount.Should().Be(0); } } /// /// Edge case tests for SkiaSlider. /// public class SkiaSliderEdgeCaseTests { [Fact] public void Value_BelowMinimum_ClampedToMinimum() { // Arrange var slider = new SkiaSlider(); slider.Maximum = 100; slider.Minimum = 10; // Act slider.Value = 5; // Assert - Value setter uses Math.Clamp(value, Minimum, Maximum) slider.Value.Should().Be(10); } [Fact] public void Value_AboveMaximum_ClampedToMaximum() { // Arrange var slider = new SkiaSlider(); slider.Minimum = 0; slider.Maximum = 50; // Act slider.Value = 100; // Assert slider.Value.Should().Be(50); } [Fact] public void Value_ValueChanged_RaisedOnChange() { // Arrange var slider = new SkiaSlider(); slider.Minimum = 0; slider.Maximum = 100; slider.Value = 0; double? oldValue = null; double? newValue = null; slider.ValueChanged += (s, e) => { oldValue = e.OldValue; newValue = e.NewValue; }; // Act slider.Value = 42; // Assert oldValue.Should().Be(0); newValue.Should().Be(42); } } /// /// Edge case tests for SkiaStackLayout. /// public class SkiaStackLayoutEdgeCaseTests { [Fact] public void AddChild_NullChild_DoesNotThrow() { // Arrange var layout = new SkiaStackLayout(); // Act & Assert - null child will cause NullReferenceException // because AddChild accesses child.Parent without null check var exception = Record.Exception(() => layout.AddChild(null!)); exception.Should().NotBeNull(); exception.Should().BeOfType(); } [Fact] public void RemoveChild_NotPresent_DoesNotThrow() { // Arrange var layout = new SkiaStackLayout(); var orphan = new SkiaLabel(); // Act & Assert - removing a child that was never added should not throw var exception = Record.Exception(() => layout.RemoveChild(orphan)); exception.Should().BeNull(); } [Fact] public void Measure_NoChildren_ReturnsZero() { // Arrange var layout = new SkiaStackLayout(); // Act var size = layout.Measure(new Size(500, 500)); // Assert - empty layout should measure to zero or very small size.Width.Should().Be(0); size.Height.Should().Be(0); } [Fact] public void Measure_WithChildren_IncludesSpacing() { // Arrange - measure with and without spacing to verify spacing is added var layoutNoSpacing = new SkiaStackLayout(); layoutNoSpacing.Orientation = Microsoft.Maui.Platform.StackOrientation.Vertical; layoutNoSpacing.Spacing = 0; var layoutWithSpacing = new SkiaStackLayout(); layoutWithSpacing.Orientation = Microsoft.Maui.Platform.StackOrientation.Vertical; layoutWithSpacing.Spacing = 10; for (int i = 0; i < 3; i++) { layoutNoSpacing.AddChild(new SkiaLabel { HeightRequest = 20, WidthRequest = 100 }); layoutWithSpacing.AddChild(new SkiaLabel { HeightRequest = 20, WidthRequest = 100 }); } // Act var sizeNoSpacing = layoutNoSpacing.Measure(new Size(500, 500)); var sizeWithSpacing = layoutWithSpacing.Measure(new Size(500, 500)); // Assert - spacing should add 2 * 10 = 20 to the height sizeWithSpacing.Height.Should().BeGreaterThan(sizeNoSpacing.Height); (sizeWithSpacing.Height - sizeNoSpacing.Height).Should().BeApproximately(20, 1); } }