All checks were successful
CI / Build (Linux) (push) Successful in 21s
Fix critical memory leaks identified in architecture review: Add signal handler disconnection in WebKitNative (load-changed and script-dialog signals now properly cleaned up), implement GTK idle callback cleanup with automatic removal on completion, add dlclose() calls for WebKit library handles, track GTK signal IDs in GtkSkiaSurfaceWidget for proper disposal. Replace empty catch blocks in GestureManager with logged exception handling. Add WebKitNative.Cleanup() and GtkNative.ClearCallbacks() methods for application shutdown.
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);
|
|
}
|
|
}
|