// 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; /// /// Minimal concrete SkiaView that uses base MeasureOverride (respects WidthRequest/HeightRequest). /// SkiaLabel overrides MeasureOverride to use font metrics, so we need this for layout tests. /// internal class TestView : SkiaView { protected override void OnDraw(SkiaSharp.SKCanvas canvas, SkiaSharp.SKRect bounds) { } } #region StackLayoutIntegrationTests public class StackLayoutIntegrationTests { [Fact] public void VerticalStack_MeasuresChildrenSequentially() { // Arrange var stack = new SkiaStackLayout { Orientation = Microsoft.Maui.Platform.StackOrientation.Vertical }; var child1 = new TestView { HeightRequest = 30, WidthRequest = 100 }; var child2 = new TestView { HeightRequest = 30, WidthRequest = 100 }; var child3 = new TestView { HeightRequest = 30, WidthRequest = 100 }; stack.AddChild(child1); stack.AddChild(child2); stack.AddChild(child3); // Act var size = stack.Measure(new Size(400, 600)); // Assert - 3 children each 30px tall => total height >= 90 size.Height.Should().BeGreaterThanOrEqualTo(90); } [Fact] public void HorizontalStack_MeasuresChildrenSideToSide() { // Arrange var stack = new SkiaStackLayout { Orientation = Microsoft.Maui.Platform.StackOrientation.Horizontal }; var child1 = new TestView { WidthRequest = 50, HeightRequest = 30 }; var child2 = new TestView { WidthRequest = 50, HeightRequest = 30 }; var child3 = new TestView { WidthRequest = 50, HeightRequest = 30 }; stack.AddChild(child1); stack.AddChild(child2); stack.AddChild(child3); // Act var size = stack.Measure(new Size(600, 400)); // Assert - 3 children each 50px wide => total width >= 150 size.Width.Should().BeGreaterThanOrEqualTo(150); } [Fact] public void VerticalStack_ArrangePositionsChildrenVertically() { // Arrange var stack = new SkiaStackLayout { Orientation = Microsoft.Maui.Platform.StackOrientation.Vertical }; var child1 = new TestView { HeightRequest = 40, WidthRequest = 100 }; var child2 = new TestView { HeightRequest = 40, WidthRequest = 100 }; var child3 = new TestView { HeightRequest = 40, WidthRequest = 100 }; stack.AddChild(child1); stack.AddChild(child2); stack.AddChild(child3); stack.Measure(new Size(400, 600)); // Act stack.Arrange(new Rect(0, 0, 400, 600)); // Assert - children should have increasing Y positions child2.Bounds.Top.Should().BeGreaterThan(child1.Bounds.Top); child3.Bounds.Top.Should().BeGreaterThan(child2.Bounds.Top); } [Fact] public void HorizontalStack_ArrangePositionsChildrenHorizontally() { // Arrange var stack = new SkiaStackLayout { Orientation = Microsoft.Maui.Platform.StackOrientation.Horizontal }; var child1 = new TestView { WidthRequest = 60, HeightRequest = 30 }; var child2 = new TestView { WidthRequest = 60, HeightRequest = 30 }; var child3 = new TestView { WidthRequest = 60, HeightRequest = 30 }; stack.AddChild(child1); stack.AddChild(child2); stack.AddChild(child3); stack.Measure(new Size(600, 400)); // Act stack.Arrange(new Rect(0, 0, 600, 400)); // Assert - children should have increasing X positions child2.Bounds.Left.Should().BeGreaterThan(child1.Bounds.Left); child3.Bounds.Left.Should().BeGreaterThan(child2.Bounds.Left); } [Fact] public void NestedStacks_MeasureCorrectly() { // Arrange var outerStack = new SkiaStackLayout { Orientation = Microsoft.Maui.Platform.StackOrientation.Vertical }; var innerStack = new SkiaStackLayout { Orientation = Microsoft.Maui.Platform.StackOrientation.Horizontal }; var innerChild1 = new TestView { WidthRequest = 50, HeightRequest = 30 }; var innerChild2 = new TestView { WidthRequest = 50, HeightRequest = 30 }; innerStack.AddChild(innerChild1); innerStack.AddChild(innerChild2); var outerChild = new TestView { WidthRequest = 100, HeightRequest = 40 }; outerStack.AddChild(innerStack); outerStack.AddChild(outerChild); // Act var size = outerStack.Measure(new Size(400, 600)); // Assert - should measure without error and produce positive size size.Width.Should().BeGreaterThan(0); size.Height.Should().BeGreaterThan(0); // Height should include both the inner stack's height and the outer child size.Height.Should().BeGreaterThanOrEqualTo(30 + 40); } [Fact] public void InvisibleChildren_ExcludedFromLayout() { // Arrange var stack = new SkiaStackLayout { Orientation = Microsoft.Maui.Platform.StackOrientation.Vertical }; var child1 = new TestView { HeightRequest = 30, WidthRequest = 100 }; var child2 = new TestView { HeightRequest = 30, WidthRequest = 100, IsVisible = false }; var child3 = new TestView { HeightRequest = 30, WidthRequest = 100 }; stack.AddChild(child1); stack.AddChild(child2); stack.AddChild(child3); // Act var size = stack.Measure(new Size(400, 600)); // Assert - only 2 visible children, so height should reflect only 2 x 30 = 60 // The invisible child should not contribute to measured height size.Height.Should().BeLessThan(90); size.Height.Should().BeGreaterThanOrEqualTo(60); } [Fact] public void Spacing_AppliedBetweenVisibleChildrenOnly() { // Arrange - 3 children with spacing=10, middle one invisible var stack = new SkiaStackLayout { Orientation = Microsoft.Maui.Platform.StackOrientation.Vertical, Spacing = 10 }; var child1 = new TestView { HeightRequest = 30, WidthRequest = 100 }; var child2 = new TestView { HeightRequest = 30, WidthRequest = 100, IsVisible = false }; var child3 = new TestView { HeightRequest = 30, WidthRequest = 100 }; stack.AddChild(child1); stack.AddChild(child2); stack.AddChild(child3); // Act var size = stack.Measure(new Size(400, 600)); // Assert - 2 visible children with 1 gap of spacing=10 // Expected: 30 + 10 + 30 = 70 size.Height.Should().Be(70); } } #endregion #region GridIntegrationTests public class GridIntegrationTests { [Fact] public void SingleCell_MeasuresChild() { // Arrange var grid = new SkiaGrid(); grid.RowDefinitions.Add(Microsoft.Maui.Platform.GridLength.Auto); grid.ColumnDefinitions.Add(Microsoft.Maui.Platform.GridLength.Auto); var button = new TestView { WidthRequest = 80, HeightRequest = 40 }; grid.AddChild(button, row: 0, column: 0); // Act var size = grid.Measure(new Size(400, 300)); // Assert - grid should measure to at least the child's requested size size.Width.Should().BeGreaterThanOrEqualTo(80); size.Height.Should().BeGreaterThanOrEqualTo(40); } [Fact] public void TwoRows_ArrangePositionsCorrectly() { // Arrange var grid = new SkiaGrid(); grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength(50)); grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength(50)); grid.ColumnDefinitions.Add(Microsoft.Maui.Platform.GridLength.Star); var child1 = new TestView { WidthRequest = 100, HeightRequest = 30 }; var child2 = new TestView { WidthRequest = 100, HeightRequest = 30 }; grid.AddChild(child1, row: 0, column: 0); grid.AddChild(child2, row: 1, column: 0); grid.Measure(new Size(400, 300)); // Act grid.Arrange(new Rect(0, 0, 400, 300)); // Assert - second row child should be at Y >= 50 child2.Bounds.Top.Should().BeGreaterThanOrEqualTo(50); } [Fact] public void TwoColumns_ArrangePositionsCorrectly() { // Arrange var grid = new SkiaGrid(); grid.RowDefinitions.Add(Microsoft.Maui.Platform.GridLength.Star); grid.ColumnDefinitions.Add(new Microsoft.Maui.Platform.GridLength(100)); grid.ColumnDefinitions.Add(new Microsoft.Maui.Platform.GridLength(100)); var child1 = new TestView { WidthRequest = 80, HeightRequest = 30 }; var child2 = new TestView { WidthRequest = 80, HeightRequest = 30 }; grid.AddChild(child1, row: 0, column: 0); grid.AddChild(child2, row: 0, column: 1); grid.Measure(new Size(400, 300)); // Act grid.Arrange(new Rect(0, 0, 400, 300)); // Assert - second column child should be at X >= 100 child2.Bounds.Left.Should().BeGreaterThanOrEqualTo(100); } [Fact] public void EmptyGrid_MeasuresWithDefinitions() { // Arrange var grid = new SkiaGrid(); grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength(60)); grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength(40)); grid.ColumnDefinitions.Add(new Microsoft.Maui.Platform.GridLength(120)); grid.ColumnDefinitions.Add(new Microsoft.Maui.Platform.GridLength(80)); // Act - no children added var size = grid.Measure(new Size(400, 300)); // Assert - should still measure based on definitions // Width = 120 + 80 = 200, Height = 60 + 40 = 100 size.Width.Should().BeGreaterThanOrEqualTo(200); size.Height.Should().BeGreaterThanOrEqualTo(100); } [Fact] public void RowSpacing_IncludedInMeasure() { // Arrange var grid = new SkiaGrid { RowSpacing = 20 }; grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength(50)); grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength(50)); grid.ColumnDefinitions.Add(Microsoft.Maui.Platform.GridLength.Star); var child1 = new TestView { WidthRequest = 100, HeightRequest = 30 }; var child2 = new TestView { WidthRequest = 100, HeightRequest = 30 }; grid.AddChild(child1, row: 0, column: 0); grid.AddChild(child2, row: 1, column: 0); // Act var size = grid.Measure(new Size(400, 300)); // Assert - height should include row spacing: 50 + 20 + 50 = 120 size.Height.Should().BeGreaterThanOrEqualTo(120); } } #endregion #region MeasureArrangePipelineTests public class MeasureArrangePipelineTests { [Fact] public void Measure_PropagatesConstraints() { // Arrange - parent with padding subtracts from available space var stack = new SkiaStackLayout { Orientation = Microsoft.Maui.Platform.StackOrientation.Vertical, Padding = new Thickness(20) }; var child = new TestView { WidthRequest = 500, HeightRequest = 50 }; stack.AddChild(child); // Act stack.Measure(new Size(400, 600)); // Assert - child's DesiredSize should reflect WidthRequest/HeightRequest child.DesiredSize.Width.Should().Be(500); child.DesiredSize.Height.Should().Be(50); } [Fact] public void Arrange_SetsBoundsOnChildren() { // Arrange var stack = new SkiaStackLayout { Orientation = Microsoft.Maui.Platform.StackOrientation.Vertical }; var child1 = new TestView { WidthRequest = 100, HeightRequest = 40 }; var child2 = new TestView { WidthRequest = 100, HeightRequest = 40 }; stack.AddChild(child1); stack.AddChild(child2); stack.Measure(new Size(400, 600)); // Act stack.Arrange(new Rect(0, 0, 400, 600)); // Assert - children should have non-zero bounds after arrange child1.Bounds.Width.Should().BeGreaterThan(0); child1.Bounds.Height.Should().BeGreaterThan(0); child2.Bounds.Width.Should().BeGreaterThan(0); child2.Bounds.Height.Should().BeGreaterThan(0); } [Fact] public void InvalidateMeasure_CausesMeasureRecalculation() { // Arrange var stack = new SkiaStackLayout { Orientation = Microsoft.Maui.Platform.StackOrientation.Vertical }; var child = new TestView { WidthRequest = 100, HeightRequest = 30 }; stack.AddChild(child); // First measure var size1 = stack.Measure(new Size(400, 600)); // Act - change child property and invalidate child.HeightRequest = 60; child.InvalidateMeasure(); // Re-measure var size2 = stack.Measure(new Size(400, 600)); // Assert - new measured size should reflect the change size2.Height.Should().BeGreaterThan(size1.Height); size2.Height.Should().BeGreaterThanOrEqualTo(60); } [Fact] public void DesiredSize_SetAfterMeasure() { // Arrange var view = new TestView { WidthRequest = 120, HeightRequest = 45 }; // Before measure, DesiredSize should be zero view.DesiredSize.Should().Be(Size.Zero); // Act var measured = view.Measure(new Size(400, 600)); // Assert - DesiredSize should reflect the measured size view.DesiredSize.Width.Should().Be(measured.Width); view.DesiredSize.Height.Should().Be(measured.Height); view.DesiredSize.Width.Should().BeGreaterThan(0); view.DesiredSize.Height.Should().BeGreaterThan(0); } } #endregion