473 lines
12 KiB
C#
473 lines
12 KiB
C#
|
|
// 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;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Edge case and boundary condition tests for SkiaView.
|
||
|
|
/// Uses concrete SkiaLabel/SkiaButton since SkiaView is abstract.
|
||
|
|
/// </summary>
|
||
|
|
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();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Edge case tests for SkiaLabel.
|
||
|
|
/// </summary>
|
||
|
|
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<LineBreakMode>())
|
||
|
|
{
|
||
|
|
var exception = Record.Exception(() => label.LineBreakMode = mode);
|
||
|
|
exception.Should().BeNull($"setting LineBreakMode to {mode} should not throw");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Edge case tests for SkiaEntry.
|
||
|
|
/// </summary>
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Edge case tests for SkiaSlider.
|
||
|
|
/// </summary>
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Edge case tests for SkiaStackLayout.
|
||
|
|
/// </summary>
|
||
|
|
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<NullReferenceException>();
|
||
|
|
}
|
||
|
|
|
||
|
|
[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);
|
||
|
|
}
|
||
|
|
}
|