Initial commit: .NET MAUI Linux Platform

Complete Linux platform implementation for .NET MAUI with:

- 35+ Skia-rendered controls (Button, Label, Entry, CarouselView, etc.)
- Platform services (Clipboard, FilePicker, Notifications, DragDrop, etc.)
- Accessibility support (AT-SPI2, High Contrast)
- HiDPI and Input Method support
- 216 unit tests
- CI/CD workflows
- Project templates
- Documentation

🤖 Generated with Claude Code
This commit is contained in:
logikonline
2025-12-19 09:30:16 +00:00
commit d87124fef2
138 changed files with 32939 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Maui.Controls.Linux.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,564 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
using Xunit;
using Microsoft.Maui.Platform.Linux.Services;
namespace Microsoft.Maui.Platform.Tests;
public class HiDpiServiceTests
{
[Fact]
public void Constructor_InitializesWithDefaultValues()
{
var service = new HiDpiService();
Assert.Equal(1.0f, service.ScaleFactor);
Assert.Equal(96f, service.Dpi);
}
[Fact]
public void Initialize_CanBeCalled()
{
var service = new HiDpiService();
service.Initialize();
// Should not throw
}
[Fact]
public void Initialize_OnlyRunsOnce()
{
var service = new HiDpiService();
service.Initialize();
service.Initialize();
// Second call should be no-op
}
[Fact]
public void DetectScaleFactor_CanBeCalled()
{
var service = new HiDpiService();
service.DetectScaleFactor();
// Should not throw, may or may not find scale
}
[Fact]
public void ToPhysicalPixels_WithDefaultScale_ReturnsInput()
{
var service = new HiDpiService();
float result = service.ToPhysicalPixels(100f);
Assert.Equal(100f, result);
}
[Fact]
public void ToLogicalPixels_WithDefaultScale_ReturnsInput()
{
var service = new HiDpiService();
float result = service.ToLogicalPixels(100f);
Assert.Equal(100f, result);
}
[Fact]
public void ScaleChangedEvent_CanBeSubscribed()
{
var service = new HiDpiService();
bool eventRaised = false;
service.ScaleChanged += (s, e) => eventRaised = true;
Assert.False(eventRaised); // Not raised yet
}
[Fact]
public void GetFontScaleFactor_ReturnsValidValue()
{
var service = new HiDpiService();
float scale = service.GetFontScaleFactor();
Assert.True(scale > 0);
}
}
public class ScaleChangedEventArgsTests
{
[Fact]
public void Constructor_SetsProperties()
{
var args = new ScaleChangedEventArgs(1.0f, 2.0f, 192f);
Assert.Equal(1.0f, args.OldScale);
Assert.Equal(2.0f, args.NewScale);
Assert.Equal(192f, args.NewDpi);
}
}
public class HighContrastServiceTests
{
[Fact]
public void Constructor_InitializesWithDefaultValues()
{
var service = new HighContrastService();
Assert.False(service.IsHighContrastEnabled);
Assert.Equal(HighContrastTheme.None, service.CurrentTheme);
}
[Fact]
public void Initialize_CanBeCalled()
{
var service = new HighContrastService();
service.Initialize();
// Should not throw
}
[Fact]
public void DetectHighContrast_CanBeCalled()
{
var service = new HighContrastService();
service.DetectHighContrast();
// Should not throw
}
[Fact]
public void ForceHighContrast_EnablesHighContrast()
{
var service = new HighContrastService();
service.ForceHighContrast(true, HighContrastTheme.WhiteOnBlack);
Assert.True(service.IsHighContrastEnabled);
Assert.Equal(HighContrastTheme.WhiteOnBlack, service.CurrentTheme);
}
[Fact]
public void ForceHighContrast_DisablesHighContrast()
{
var service = new HighContrastService();
service.ForceHighContrast(true);
service.ForceHighContrast(false);
Assert.False(service.IsHighContrastEnabled);
}
[Fact]
public void GetColors_ReturnsWhiteOnBlackColors()
{
var service = new HighContrastService();
service.ForceHighContrast(true, HighContrastTheme.WhiteOnBlack);
var colors = service.GetColors();
Assert.Equal(SKColors.Black, colors.Background);
Assert.Equal(SKColors.White, colors.Foreground);
}
[Fact]
public void GetColors_ReturnsBlackOnWhiteColors()
{
var service = new HighContrastService();
service.ForceHighContrast(true, HighContrastTheme.BlackOnWhite);
var colors = service.GetColors();
Assert.Equal(SKColors.White, colors.Background);
Assert.Equal(SKColors.Black, colors.Foreground);
}
[Fact]
public void GetColors_ReturnsDefaultColorsWhenDisabled()
{
var service = new HighContrastService();
var colors = service.GetColors();
Assert.Equal(SKColors.White, colors.Background);
Assert.NotEqual(SKColors.Black, colors.Foreground); // Default is gray
}
[Fact]
public void HighContrastChangedEvent_CanBeSubscribed()
{
var service = new HighContrastService();
bool eventRaised = false;
service.HighContrastChanged += (s, e) => eventRaised = true;
service.ForceHighContrast(true);
Assert.True(eventRaised);
}
}
public class HighContrastColorsTests
{
[Fact]
public void AllPropertiesCanBeSet()
{
var colors = new HighContrastColors
{
Background = SKColors.Black,
Foreground = SKColors.White,
Accent = SKColors.Cyan,
Border = SKColors.White,
Error = SKColors.Red,
Success = SKColors.Green,
Warning = SKColors.Yellow,
Link = SKColors.Blue,
LinkVisited = SKColors.Purple,
Selection = SKColors.Blue,
SelectionText = SKColors.White,
DisabledText = SKColors.Gray,
DisabledBackground = SKColors.DarkGray
};
Assert.Equal(SKColors.Black, colors.Background);
Assert.Equal(SKColors.White, colors.Foreground);
Assert.Equal(SKColors.Cyan, colors.Accent);
}
}
public class HighContrastChangedEventArgsTests
{
[Fact]
public void Constructor_SetsProperties()
{
var args = new HighContrastChangedEventArgs(true, HighContrastTheme.WhiteOnBlack);
Assert.True(args.IsEnabled);
Assert.Equal(HighContrastTheme.WhiteOnBlack, args.Theme);
}
}
public class DragDropServiceTests
{
[Fact]
public void Constructor_InitializesWithDefaultValues()
{
var service = new DragDropService();
Assert.False(service.IsDragging);
}
[Fact]
public void CancelDrag_WhenNotDragging_DoesNotThrow()
{
var service = new DragDropService();
service.CancelDrag();
Assert.False(service.IsDragging);
}
[Fact]
public void ProcessClientMessage_CanBeCalled()
{
var service = new DragDropService();
// Simply verify the method can be called without exception
service.ProcessClientMessage(0, new nint[5]);
Assert.NotNull(service);
}
[Fact]
public void DragEnterEvent_CanBeSubscribed()
{
var service = new DragDropService();
bool eventRaised = false;
service.DragEnter += (s, e) => eventRaised = true;
Assert.False(eventRaised);
}
[Fact]
public void DragOverEvent_CanBeSubscribed()
{
var service = new DragDropService();
bool eventRaised = false;
service.DragOver += (s, e) => eventRaised = true;
Assert.False(eventRaised);
}
[Fact]
public void DragLeaveEvent_CanBeSubscribed()
{
var service = new DragDropService();
bool eventRaised = false;
service.DragLeave += (s, e) => eventRaised = true;
Assert.False(eventRaised);
}
[Fact]
public void DropEvent_CanBeSubscribed()
{
var service = new DragDropService();
bool eventRaised = false;
service.Drop += (s, e) => eventRaised = true;
Assert.False(eventRaised);
}
[Fact]
public void Dispose_CanBeCalled()
{
var service = new DragDropService();
service.Dispose();
// Should not throw
}
[Fact]
public void Dispose_CanBeCalledMultipleTimes()
{
var service = new DragDropService();
service.Dispose();
service.Dispose();
// Should not throw
}
}
public class DragDataTests
{
[Fact]
public void Constructor_InitializesWithDefaultValues()
{
var data = new DragData();
Assert.Equal(IntPtr.Zero, data.SourceWindow);
Assert.Empty(data.SupportedTypes);
Assert.Null(data.Text);
Assert.Null(data.FilePaths);
Assert.Null(data.Data);
}
[Fact]
public void Text_CanBeSet()
{
var data = new DragData { Text = "Hello World" };
Assert.Equal("Hello World", data.Text);
}
[Fact]
public void FilePaths_CanBeSet()
{
var data = new DragData
{
FilePaths = new[] { "/home/user/file1.txt", "/home/user/file2.txt" }
};
Assert.Equal(2, data.FilePaths.Length);
}
[Fact]
public void Data_CanBeSet()
{
var customData = new { Id = 1, Name = "Test" };
var data = new DragData { Data = customData };
Assert.Equal(customData, data.Data);
}
}
public class LinuxDragEventArgsTests
{
[Fact]
public void Constructor_SetsProperties()
{
var dragData = new DragData { Text = "Test" };
var args = new Microsoft.Maui.Platform.Linux.Services.DragEventArgs(dragData, 100, 200);
Assert.Equal(dragData, args.Data);
Assert.Equal(100, args.X);
Assert.Equal(200, args.Y);
Assert.False(args.Accepted);
}
[Fact]
public void Accepted_CanBeSet()
{
var args = new Microsoft.Maui.Platform.Linux.Services.DragEventArgs(new DragData(), 0, 0);
args.Accepted = true;
Assert.True(args.Accepted);
}
[Fact]
public void AllowedAction_CanBeSet()
{
var args = new Microsoft.Maui.Platform.Linux.Services.DragEventArgs(new DragData(), 0, 0);
args.AllowedAction = DragAction.Move;
Assert.Equal(DragAction.Move, args.AllowedAction);
}
}
public class LinuxDropEventArgsTests
{
[Fact]
public void Constructor_SetsProperties()
{
var dragData = new DragData();
var args = new Microsoft.Maui.Platform.Linux.Services.DropEventArgs(dragData, "dropped content");
Assert.Equal(dragData, args.Data);
Assert.Equal("dropped content", args.DroppedData);
Assert.False(args.Handled);
}
[Fact]
public void Handled_CanBeSet()
{
var args = new Microsoft.Maui.Platform.Linux.Services.DropEventArgs(new DragData(), null);
args.Handled = true;
Assert.True(args.Handled);
}
}
public class GlobalHotkeyServiceTests
{
[Fact]
public void Constructor_DoesNotThrow()
{
var service = new GlobalHotkeyService();
Assert.NotNull(service);
}
[Fact]
public void HotkeyPressedEvent_CanBeSubscribed()
{
var service = new GlobalHotkeyService();
bool eventRaised = false;
service.HotkeyPressed += (s, e) => eventRaised = true;
Assert.False(eventRaised);
}
[Fact]
public void UnregisterAll_WhenNoRegistrations_DoesNotThrow()
{
var service = new GlobalHotkeyService();
service.UnregisterAll();
// Should not throw
}
[Fact]
public void Dispose_CanBeCalled()
{
var service = new GlobalHotkeyService();
service.Dispose();
// Should not throw
}
}
public class HotkeyEventArgsTests
{
[Fact]
public void Constructor_SetsProperties()
{
var args = new HotkeyEventArgs(1, HotkeyKey.A, HotkeyModifiers.Control);
Assert.Equal(1, args.Id);
Assert.Equal(HotkeyKey.A, args.Key);
Assert.Equal(HotkeyModifiers.Control, args.Modifiers);
}
[Fact]
public void ModifiersCanBeCombined()
{
var modifiers = HotkeyModifiers.Control | HotkeyModifiers.Shift;
var args = new HotkeyEventArgs(1, HotkeyKey.S, modifiers);
Assert.True(args.Modifiers.HasFlag(HotkeyModifiers.Control));
Assert.True(args.Modifiers.HasFlag(HotkeyModifiers.Shift));
}
}
public class HotkeyModifiersTests
{
[Fact]
public void None_IsZero()
{
Assert.Equal(0, (int)HotkeyModifiers.None);
}
[Fact]
public void AllModifiers_AreFlagBased()
{
var all = HotkeyModifiers.Shift | HotkeyModifiers.Control | HotkeyModifiers.Alt | HotkeyModifiers.Super;
Assert.True(all.HasFlag(HotkeyModifiers.Shift));
Assert.True(all.HasFlag(HotkeyModifiers.Control));
Assert.True(all.HasFlag(HotkeyModifiers.Alt));
Assert.True(all.HasFlag(HotkeyModifiers.Super));
}
}
public class HotkeyKeyTests
{
[Fact]
public void Letters_HaveCorrectValues()
{
Assert.Equal((uint)0x61, (uint)HotkeyKey.A);
Assert.Equal((uint)0x7A, (uint)HotkeyKey.Z);
}
[Fact]
public void FunctionKeys_HaveCorrectValues()
{
Assert.Equal((uint)0xFFBE, (uint)HotkeyKey.F1);
Assert.Equal((uint)0xFFC9, (uint)HotkeyKey.F12);
}
[Fact]
public void SpecialKeys_HaveCorrectValues()
{
Assert.Equal((uint)0xFF1B, (uint)HotkeyKey.Escape);
Assert.Equal((uint)0x20, (uint)HotkeyKey.Space);
Assert.Equal((uint)0xFF0D, (uint)HotkeyKey.Return);
}
[Fact]
public void ArrowKeys_HaveCorrectValues()
{
Assert.Equal((uint)0xFF51, (uint)HotkeyKey.Left);
Assert.Equal((uint)0xFF52, (uint)HotkeyKey.Up);
Assert.Equal((uint)0xFF53, (uint)HotkeyKey.Right);
Assert.Equal((uint)0xFF54, (uint)HotkeyKey.Down);
}
}

View File

@@ -0,0 +1,140 @@
// 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.Platform;
using SkiaSharp;
using Xunit;
namespace Microsoft.Maui.Controls.Linux.Tests.Views;
public class SkiaButtonTests
{
[Fact]
public void Constructor_SetsDefaultValues()
{
// Arrange & Act
var button = new SkiaButton();
// Assert
button.Text.Should().BeEmpty();
button.IsEnabled.Should().BeTrue();
button.IsFocusable.Should().BeTrue();
}
[Fact]
public void Text_WhenSet_UpdatesProperty()
{
// Arrange
var button = new SkiaButton();
// Act
button.Text = "Click Me";
// Assert
button.Text.Should().Be("Click Me");
}
[Fact]
public void Measure_ReturnsMinimumSize()
{
// Arrange
var button = new SkiaButton { Text = "Test" };
// Act
var size = button.Measure(new SKSize(1000, 1000));
// Assert
size.Width.Should().BeGreaterThan(0);
size.Height.Should().BeGreaterThan(0);
}
[Fact]
public void Measure_ReturnsPositiveSize()
{
// Arrange
var button = new SkiaButton
{
Text = "Test",
RequestedWidth = 200,
RequestedHeight = 50
};
// Act
var size = button.Measure(new SKSize(1000, 1000));
// Assert - Measure returns content-based size
size.Width.Should().BeGreaterThan(0);
size.Height.Should().BeGreaterThan(0);
}
[Fact]
public void IsEnabled_WhenFalse_UpdatesProperty()
{
// Arrange
var button = new SkiaButton { Text = "Test" };
// Act
button.IsEnabled = false;
// Assert
button.IsEnabled.Should().BeFalse();
}
[Fact]
public void Clicked_EventCanBeSubscribed()
{
// Arrange
var button = new SkiaButton { Text = "Test" };
var eventSubscribed = false;
// Act
button.Clicked += (s, e) => eventSubscribed = true;
// Assert - Just verify we can subscribe without error
eventSubscribed.Should().BeFalse(); // Not raised yet, just subscribed
}
[Fact]
public void Draw_DoesNotThrow()
{
// Arrange
var button = new SkiaButton { Text = "Test" };
button.Bounds = new SKRect(0, 0, 100, 40);
using var surface = SKSurface.Create(new SKImageInfo(200, 100));
var canvas = surface.Canvas;
// Act & Assert
var exception = Record.Exception(() => button.Draw(canvas));
exception.Should().BeNull();
}
[Fact]
public void TextColor_WhenSet_UpdatesProperty()
{
// Arrange
var button = new SkiaButton();
var color = new SKColor(255, 0, 0);
// Act
button.TextColor = color;
// Assert
button.TextColor.Should().Be(color);
}
[Fact]
public void BackgroundColor_WhenSet_UpdatesProperty()
{
// Arrange
var button = new SkiaButton();
var color = new SKColor(0, 255, 0);
// Act
button.BackgroundColor = color;
// Assert
button.BackgroundColor.Should().Be(color);
}
}

View File

@@ -0,0 +1,266 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
using Xunit;
namespace Microsoft.Maui.Platform.Tests;
public class SkiaCarouselViewTests
{
[Fact]
public void Constructor_InitializesWithDefaultValues()
{
var carousel = new SkiaCarouselView();
Assert.Equal(0, carousel.Position);
Assert.False(carousel.Loop);
Assert.Equal(0f, carousel.PeekAreaInsets);
Assert.Equal(0, carousel.ItemCount);
}
[Fact]
public void Position_CanBeSet()
{
var carousel = new SkiaCarouselView();
carousel.AddItem(new SkiaLabel { Text = "Item1" });
carousel.AddItem(new SkiaLabel { Text = "Item2" });
carousel.AddItem(new SkiaLabel { Text = "Item3" });
carousel.Position = 2;
Assert.Equal(2, carousel.Position);
}
[Fact]
public void Position_RaisesPositionChangedEvent()
{
var carousel = new SkiaCarouselView();
carousel.AddItem(new SkiaLabel { Text = "Item1" });
carousel.AddItem(new SkiaLabel { Text = "Item2" });
int eventRaised = 0;
int previousPos = -1;
int currentPos = -1;
carousel.PositionChanged += (s, e) =>
{
eventRaised++;
previousPos = e.PreviousPosition;
currentPos = e.CurrentPosition;
};
carousel.Position = 1;
Assert.Equal(1, eventRaised);
Assert.Equal(0, previousPos);
Assert.Equal(1, currentPos);
}
[Fact]
public void Loop_CanBeEnabled()
{
var carousel = new SkiaCarouselView();
carousel.Loop = true;
Assert.True(carousel.Loop);
}
[Fact]
public void PeekAreaInsets_CanBeSet()
{
var carousel = new SkiaCarouselView();
carousel.PeekAreaInsets = 20f;
Assert.Equal(20f, carousel.PeekAreaInsets);
}
[Fact]
public void AddItem_IncreasesItemCount()
{
var carousel = new SkiaCarouselView();
carousel.AddItem(new SkiaLabel { Text = "Item1" });
carousel.AddItem(new SkiaLabel { Text = "Item2" });
Assert.Equal(2, carousel.ItemCount);
}
[Fact]
public void RemoveItem_DecreasesItemCount()
{
var carousel = new SkiaCarouselView();
var item = new SkiaLabel { Text = "Item1" };
carousel.AddItem(item);
carousel.AddItem(new SkiaLabel { Text = "Item2" });
carousel.RemoveItem(item);
Assert.Equal(1, carousel.ItemCount);
}
[Fact]
public void ClearItems_RemovesAllItems()
{
var carousel = new SkiaCarouselView();
carousel.AddItem(new SkiaLabel { Text = "Item1" });
carousel.AddItem(new SkiaLabel { Text = "Item2" });
carousel.ClearItems();
Assert.Equal(0, carousel.ItemCount);
}
[Fact]
public void ScrollTo_UpdatesPosition()
{
var carousel = new SkiaCarouselView();
carousel.AddItem(new SkiaLabel { Text = "A" });
carousel.AddItem(new SkiaLabel { Text = "B" });
carousel.AddItem(new SkiaLabel { Text = "C" });
carousel.AddItem(new SkiaLabel { Text = "D" });
carousel.ScrollTo(2);
Assert.Equal(2, carousel.Position);
}
[Fact]
public void ScrollTo_WithAnimation_UpdatesPosition()
{
var carousel = new SkiaCarouselView();
carousel.AddItem(new SkiaLabel { Text = "A" });
carousel.AddItem(new SkiaLabel { Text = "B" });
carousel.AddItem(new SkiaLabel { Text = "C" });
carousel.ScrollTo(1, animate: true);
Assert.Equal(1, carousel.Position);
}
[Fact]
public void IsSwipeEnabled_DefaultsToTrue()
{
var carousel = new SkiaCarouselView();
Assert.True(carousel.IsSwipeEnabled);
}
[Fact]
public void IsSwipeEnabled_CanBeDisabled()
{
var carousel = new SkiaCarouselView();
carousel.IsSwipeEnabled = false;
Assert.False(carousel.IsSwipeEnabled);
}
[Fact]
public void ShowIndicators_DefaultsToTrue()
{
var carousel = new SkiaCarouselView();
Assert.True(carousel.ShowIndicators);
}
[Fact]
public void ShowIndicators_CanBeDisabled()
{
var carousel = new SkiaCarouselView();
carousel.ShowIndicators = false;
Assert.False(carousel.ShowIndicators);
}
[Fact]
public void IndicatorColor_CanBeSet()
{
var carousel = new SkiaCarouselView();
carousel.IndicatorColor = SKColors.Gray;
Assert.Equal(SKColors.Gray, carousel.IndicatorColor);
}
[Fact]
public void SelectedIndicatorColor_CanBeSet()
{
var carousel = new SkiaCarouselView();
carousel.SelectedIndicatorColor = SKColors.Blue;
Assert.Equal(SKColors.Blue, carousel.SelectedIndicatorColor);
}
[Fact]
public void ScrolledEvent_CanBeSubscribed()
{
var carousel = new SkiaCarouselView();
carousel.AddItem(new SkiaLabel { Text = "1" });
carousel.AddItem(new SkiaLabel { Text = "2" });
bool subscribed = false;
carousel.Scrolled += (s, e) => subscribed = true;
Assert.NotNull(carousel); // Event can be subscribed
}
[Fact]
public void ItemSpacing_DefaultsToZero()
{
var carousel = new SkiaCarouselView();
Assert.Equal(0f, carousel.ItemSpacing);
}
[Fact]
public void ItemSpacing_CanBeSet()
{
var carousel = new SkiaCarouselView();
carousel.ItemSpacing = 16f;
Assert.Equal(16f, carousel.ItemSpacing);
}
[Fact]
public void Position_NotChangedWhenOutOfRange()
{
var carousel = new SkiaCarouselView();
carousel.AddItem(new SkiaLabel { Text = "Item1" });
carousel.AddItem(new SkiaLabel { Text = "Item2" });
carousel.Position = 1;
carousel.Position = 10; // Out of range
// Position stays at previous valid value or is clamped
Assert.True(carousel.Position >= 0 && carousel.Position < carousel.ItemCount);
}
[Fact]
public void HitTest_ReturnsCorrectView()
{
var carousel = new SkiaCarouselView();
carousel.AddItem(new SkiaLabel { Text = "Item" });
carousel.Arrange(new SKRect(0, 0, 300, 200));
var hit = carousel.HitTest(150, 100);
Assert.NotNull(hit);
}
}
public class PositionChangedEventArgsTests
{
[Fact]
public void Constructor_SetsProperties()
{
var args = new PositionChangedEventArgs(0, 2);
Assert.Equal(0, args.PreviousPosition);
Assert.Equal(2, args.CurrentPosition);
}
}

View File

@@ -0,0 +1,190 @@
// 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.Platform;
using SkiaSharp;
using Xunit;
namespace Microsoft.Maui.Controls.Linux.Tests.Views;
public class SkiaEntryTests
{
[Fact]
public void Constructor_SetsDefaultValues()
{
// Arrange & Act
var entry = new SkiaEntry();
// Assert
entry.Text.Should().BeEmpty();
entry.Placeholder.Should().BeEmpty();
entry.IsEnabled.Should().BeTrue();
entry.IsReadOnly.Should().BeFalse();
entry.IsFocusable.Should().BeTrue();
}
[Fact]
public void Text_WhenSet_UpdatesProperty()
{
// Arrange
var entry = new SkiaEntry();
// Act
entry.Text = "Hello World";
// Assert
entry.Text.Should().Be("Hello World");
}
[Fact]
public void Text_WhenSet_RaisesTextChangedEvent()
{
// Arrange
var entry = new SkiaEntry();
string? oldText = null;
string? newText = null;
entry.TextChanged += (s, e) =>
{
oldText = e.OldTextValue;
newText = e.NewTextValue;
};
// Act
entry.Text = "Test";
// Assert
oldText.Should().BeEmpty();
newText.Should().Be("Test");
}
[Fact]
public void Placeholder_WhenSet_UpdatesProperty()
{
// Arrange
var entry = new SkiaEntry();
// Act
entry.Placeholder = "Enter text...";
// Assert
entry.Placeholder.Should().Be("Enter text...");
}
[Fact]
public void IsPassword_WhenTrue_MasksText()
{
// Arrange
var entry = new SkiaEntry
{
Text = "secret",
IsPassword = true
};
// Assert
entry.IsPassword.Should().BeTrue();
// The actual masking is done in Draw, but we verify the property is set
}
[Fact]
public void MaxLength_CanBeSet()
{
// Arrange
var entry = new SkiaEntry();
// Act
entry.MaxLength = 5;
// Assert
entry.MaxLength.Should().Be(5);
}
[Fact]
public void OnTextInput_ModifiesText()
{
// Arrange
var entry = new SkiaEntry { Text = "Hello" };
entry.Bounds = new SKRect(0, 0, 200, 40);
entry.OnFocusGained();
var originalLength = entry.Text.Length;
// Act
entry.OnTextInput(new TextInputEventArgs(" World"));
// Assert - Text is modified (inserted at cursor position)
entry.Text.Length.Should().BeGreaterThan(originalLength);
}
[Fact]
public void OnKeyDown_ReturnsKeyEvent()
{
// Arrange
var entry = new SkiaEntry { Text = "Hello" };
entry.Bounds = new SKRect(0, 0, 200, 40);
entry.OnFocusGained();
// Act - Verify OnKeyDown doesn't throw
var exception = Record.Exception(() => entry.OnKeyDown(new KeyEventArgs(Key.Backspace)));
// Assert
exception.Should().BeNull();
}
[Fact]
public void OnKeyDown_WhenReadOnly_TextRemainsSame()
{
// Arrange
var entry = new SkiaEntry { Text = "Hello", IsReadOnly = true };
var originalText = entry.Text;
entry.Bounds = new SKRect(0, 0, 200, 40);
entry.OnFocusGained();
// Act
entry.OnKeyDown(new KeyEventArgs(Key.Backspace));
// Assert - Text should remain unchanged
entry.Text.Should().Be(originalText);
}
[Fact]
public void CursorPosition_CanBeSet()
{
// Arrange
var entry = new SkiaEntry { Text = "Hello World" };
// Act
entry.CursorPosition = 5;
// Assert
entry.CursorPosition.Should().Be(5);
}
[Fact]
public void Draw_DoesNotThrow()
{
// Arrange
var entry = new SkiaEntry { Text = "Test", Placeholder = "Enter..." };
entry.Bounds = new SKRect(0, 0, 200, 40);
using var surface = SKSurface.Create(new SKImageInfo(300, 100));
var canvas = surface.Canvas;
// Act & Assert
var exception = Record.Exception(() => entry.Draw(canvas));
exception.Should().BeNull();
}
[Fact]
public void SelectAll_SelectsEntireText()
{
// Arrange
var entry = new SkiaEntry { Text = "Hello World" };
entry.OnFocusGained();
// Act
entry.SelectAll();
// Assert
entry.SelectionLength.Should().Be(11);
}
}

View File

@@ -0,0 +1,271 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
using Xunit;
namespace Microsoft.Maui.Platform.Tests;
public class SkiaIndicatorViewTests
{
[Fact]
public void Constructor_InitializesWithDefaultValues()
{
var indicator = new SkiaIndicatorView();
Assert.Equal(0, indicator.Count);
Assert.Equal(0, indicator.Position);
Assert.Equal(IndicatorShape.Circle, indicator.IndicatorShape);
}
[Fact]
public void Count_CanBeSet()
{
var indicator = new SkiaIndicatorView();
indicator.Count = 5;
Assert.Equal(5, indicator.Count);
}
[Fact]
public void Position_CanBeSet()
{
var indicator = new SkiaIndicatorView();
indicator.Count = 5;
indicator.Position = 3;
Assert.Equal(3, indicator.Position);
}
[Fact]
public void Position_ClampedToCount()
{
var indicator = new SkiaIndicatorView();
indicator.Count = 3;
indicator.Position = 10;
Assert.Equal(2, indicator.Position); // Clamped to max (Count - 1)
}
[Fact]
public void Position_ClampedToZero()
{
var indicator = new SkiaIndicatorView();
indicator.Count = 3;
indicator.Position = -5;
Assert.Equal(0, indicator.Position);
}
[Fact]
public void IndicatorColor_CanBeSet()
{
var indicator = new SkiaIndicatorView();
indicator.IndicatorColor = SKColors.Gray;
Assert.Equal(SKColors.Gray, indicator.IndicatorColor);
}
[Fact]
public void SelectedIndicatorColor_CanBeSet()
{
var indicator = new SkiaIndicatorView();
indicator.SelectedIndicatorColor = SKColors.Blue;
Assert.Equal(SKColors.Blue, indicator.SelectedIndicatorColor);
}
[Fact]
public void IndicatorSize_CanBeSet()
{
var indicator = new SkiaIndicatorView();
indicator.IndicatorSize = 12f;
Assert.Equal(12f, indicator.IndicatorSize);
}
[Fact]
public void SelectedIndicatorSize_CanBeSet()
{
var indicator = new SkiaIndicatorView();
indicator.SelectedIndicatorSize = 16f;
Assert.Equal(16f, indicator.SelectedIndicatorSize);
}
[Fact]
public void IndicatorSpacing_CanBeSet()
{
var indicator = new SkiaIndicatorView();
indicator.IndicatorSpacing = 10f;
Assert.Equal(10f, indicator.IndicatorSpacing);
}
[Fact]
public void IndicatorShape_CanBeSetToSquare()
{
var indicator = new SkiaIndicatorView();
indicator.IndicatorShape = IndicatorShape.Square;
Assert.Equal(IndicatorShape.Square, indicator.IndicatorShape);
}
[Fact]
public void IndicatorShape_CanBeSetToRoundedSquare()
{
var indicator = new SkiaIndicatorView();
indicator.IndicatorShape = IndicatorShape.RoundedSquare;
Assert.Equal(IndicatorShape.RoundedSquare, indicator.IndicatorShape);
}
[Fact]
public void IndicatorShape_CanBeSetToDiamond()
{
var indicator = new SkiaIndicatorView();
indicator.IndicatorShape = IndicatorShape.Diamond;
Assert.Equal(IndicatorShape.Diamond, indicator.IndicatorShape);
}
[Fact]
public void ShowBorder_DefaultsToFalse()
{
var indicator = new SkiaIndicatorView();
Assert.False(indicator.ShowBorder);
}
[Fact]
public void ShowBorder_CanBeEnabled()
{
var indicator = new SkiaIndicatorView();
indicator.ShowBorder = true;
Assert.True(indicator.ShowBorder);
}
[Fact]
public void BorderColor_CanBeSet()
{
var indicator = new SkiaIndicatorView();
indicator.BorderColor = SKColors.Black;
Assert.Equal(SKColors.Black, indicator.BorderColor);
}
[Fact]
public void BorderWidth_CanBeSet()
{
var indicator = new SkiaIndicatorView();
indicator.BorderWidth = 2f;
Assert.Equal(2f, indicator.BorderWidth);
}
[Fact]
public void MaximumVisible_DefaultValue()
{
var indicator = new SkiaIndicatorView();
Assert.Equal(10, indicator.MaximumVisible);
}
[Fact]
public void MaximumVisible_CanBeSet()
{
var indicator = new SkiaIndicatorView();
indicator.MaximumVisible = 5;
Assert.Equal(5, indicator.MaximumVisible);
}
[Fact]
public void HideSingle_DefaultsToTrue()
{
var indicator = new SkiaIndicatorView();
Assert.True(indicator.HideSingle);
}
[Fact]
public void HideSingle_CanBeDisabled()
{
var indicator = new SkiaIndicatorView();
indicator.HideSingle = false;
Assert.False(indicator.HideSingle);
}
[Fact]
public void Count_AdjustsPosition_WhenReduced()
{
var indicator = new SkiaIndicatorView();
indicator.Count = 5;
indicator.Position = 4;
indicator.Count = 3;
Assert.Equal(2, indicator.Position); // Adjusted to max valid
}
[Fact]
public void HitTest_OnIndicator_ReturnsView()
{
var indicator = new SkiaIndicatorView();
indicator.Count = 5;
indicator.IndicatorSize = 10f;
indicator.IndicatorSpacing = 8f;
indicator.Arrange(new SKRect(0, 0, 200, 20));
var hit = indicator.HitTest(100, 10);
Assert.NotNull(hit);
}
[Fact]
public void IsVisible_DefaultsToTrue()
{
var indicator = new SkiaIndicatorView();
Assert.True(indicator.IsVisible);
}
[Fact]
public void IsEnabled_DefaultsToTrue()
{
var indicator = new SkiaIndicatorView();
Assert.True(indicator.IsEnabled);
}
}
public class IndicatorShapeTests
{
[Fact]
public void AllShapesAreDefined()
{
Assert.Equal(IndicatorShape.Circle, (IndicatorShape)0);
Assert.Equal(IndicatorShape.Square, (IndicatorShape)1);
Assert.Equal(IndicatorShape.RoundedSquare, (IndicatorShape)2);
Assert.Equal(IndicatorShape.Diamond, (IndicatorShape)3);
}
}

View File

@@ -0,0 +1,366 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
using Xunit;
namespace Microsoft.Maui.Platform.Tests;
public class SkiaMenuBarTests
{
[Fact]
public void Constructor_InitializesWithDefaultValues()
{
var menuBar = new SkiaMenuBar();
Assert.Empty(menuBar.Items);
Assert.Equal(28f, menuBar.BarHeight);
Assert.Equal(13f, menuBar.FontSize);
}
[Fact]
public void Items_CanAddMenuBarItem()
{
var menuBar = new SkiaMenuBar();
var item = new MenuBarItem { Text = "File" };
menuBar.Items.Add(item);
Assert.Single(menuBar.Items);
Assert.Equal("File", menuBar.Items[0].Text);
}
[Fact]
public void MenuBarItem_CanAddMenuItems()
{
var menuBarItem = new MenuBarItem { Text = "Edit" };
menuBarItem.Items.Add(new MenuItem { Text = "Cut" });
menuBarItem.Items.Add(new MenuItem { Text = "Copy" });
menuBarItem.Items.Add(new MenuItem { Text = "Paste" });
Assert.Equal(3, menuBarItem.Items.Count);
}
[Fact]
public void MenuItem_CanHaveShortcut()
{
var item = new MenuItem { Text = "Save", Shortcut = "Ctrl+S" };
Assert.Equal("Ctrl+S", item.Shortcut);
}
[Fact]
public void MenuItem_CanBeSeparator()
{
var item = new MenuItem { IsSeparator = true };
Assert.True(item.IsSeparator);
}
[Fact]
public void MenuItem_IsEnabled_DefaultsToTrue()
{
var item = new MenuItem { Text = "Test" };
Assert.True(item.IsEnabled);
}
[Fact]
public void MenuItem_CanBeDisabled()
{
var item = new MenuItem { Text = "Test", IsEnabled = false };
Assert.False(item.IsEnabled);
}
[Fact]
public void MenuItem_CanBeChecked()
{
var item = new MenuItem { Text = "Option", IsChecked = true };
Assert.True(item.IsChecked);
}
[Fact]
public void MenuItem_CanHaveIcon()
{
var item = new MenuItem { Text = "Open", IconSource = "open.png" };
Assert.Equal("open.png", item.IconSource);
}
[Fact]
public void MenuItem_CanHaveSubItems()
{
var item = new MenuItem { Text = "Recent" };
item.SubItems.Add(new MenuItem { Text = "File1.txt" });
item.SubItems.Add(new MenuItem { Text = "File2.txt" });
Assert.Equal(2, item.SubItems.Count);
}
[Fact]
public void MenuItem_ClickedEvent_CanBeSubscribed()
{
var item = new MenuItem { Text = "Test" };
bool clicked = false;
item.Clicked += (s, e) => clicked = true;
Assert.False(clicked); // Event not raised yet
}
[Fact]
public void BarHeight_CanBeSet()
{
var menuBar = new SkiaMenuBar();
menuBar.BarHeight = 32f;
Assert.Equal(32f, menuBar.BarHeight);
}
[Fact]
public void FontSize_CanBeSet()
{
var menuBar = new SkiaMenuBar();
menuBar.FontSize = 14f;
Assert.Equal(14f, menuBar.FontSize);
}
[Fact]
public void ItemPadding_CanBeSet()
{
var menuBar = new SkiaMenuBar();
menuBar.ItemPadding = 16f;
Assert.Equal(16f, menuBar.ItemPadding);
}
[Fact]
public void BackgroundColor_CanBeSet()
{
var menuBar = new SkiaMenuBar();
menuBar.BackgroundColor = SKColors.White;
Assert.Equal(SKColors.White, menuBar.BackgroundColor);
}
[Fact]
public void TextColor_CanBeSet()
{
var menuBar = new SkiaMenuBar();
menuBar.TextColor = SKColors.Black;
Assert.Equal(SKColors.Black, menuBar.TextColor);
}
[Fact]
public void HoverBackgroundColor_CanBeSet()
{
var menuBar = new SkiaMenuBar();
menuBar.HoverBackgroundColor = SKColors.LightGray;
Assert.Equal(SKColors.LightGray, menuBar.HoverBackgroundColor);
}
[Fact]
public void ActiveBackgroundColor_CanBeSet()
{
var menuBar = new SkiaMenuBar();
menuBar.ActiveBackgroundColor = SKColors.DarkGray;
Assert.Equal(SKColors.DarkGray, menuBar.ActiveBackgroundColor);
}
[Fact]
public void Measure_ReturnsBarHeight()
{
var menuBar = new SkiaMenuBar();
menuBar.BarHeight = 30f;
var size = menuBar.Measure(new SKSize(800, 600));
Assert.Equal(30f, size.Height);
}
[Fact]
public void Measure_ReturnsAvailableWidth()
{
var menuBar = new SkiaMenuBar();
var size = menuBar.Measure(new SKSize(800, 600));
Assert.Equal(800f, size.Width);
}
[Fact]
public void HitTest_WithinBounds_ReturnsMenuBar()
{
var menuBar = new SkiaMenuBar();
menuBar.Arrange(new SKRect(0, 0, 800, 28));
var hit = menuBar.HitTest(400, 14);
Assert.Equal(menuBar, hit);
}
[Fact]
public void HitTest_OutsideBounds_ReturnsNull()
{
var menuBar = new SkiaMenuBar();
menuBar.Arrange(new SKRect(0, 0, 800, 28));
var hit = menuBar.HitTest(400, 50);
Assert.Null(hit);
}
}
public class SkiaMenuFlyoutTests
{
[Fact]
public void Constructor_InitializesWithDefaultValues()
{
var flyout = new SkiaMenuFlyout();
Assert.Empty(flyout.Items);
Assert.Equal(13f, flyout.FontSize);
Assert.Equal(28f, flyout.ItemHeight);
Assert.Equal(180f, flyout.MinWidth);
}
[Fact]
public void Items_CanBeSet()
{
var flyout = new SkiaMenuFlyout();
flyout.Items = new List<MenuItem>
{
new() { Text = "Item1" },
new() { Text = "Item2" }
};
Assert.Equal(2, flyout.Items.Count);
}
[Fact]
public void Position_CanBeSet()
{
var flyout = new SkiaMenuFlyout();
flyout.Position = new SKPoint(100, 200);
Assert.Equal(100, flyout.Position.X);
Assert.Equal(200, flyout.Position.Y);
}
[Fact]
public void BackgroundColor_CanBeSet()
{
var flyout = new SkiaMenuFlyout();
flyout.BackgroundColor = SKColors.White;
Assert.Equal(SKColors.White, flyout.BackgroundColor);
}
[Fact]
public void TextColor_CanBeSet()
{
var flyout = new SkiaMenuFlyout();
flyout.TextColor = SKColors.Black;
Assert.Equal(SKColors.Black, flyout.TextColor);
}
[Fact]
public void DisabledTextColor_CanBeSet()
{
var flyout = new SkiaMenuFlyout();
flyout.DisabledTextColor = new SKColor(160, 160, 160);
Assert.Equal(new SKColor(160, 160, 160), flyout.DisabledTextColor);
}
[Fact]
public void HoverBackgroundColor_CanBeSet()
{
var flyout = new SkiaMenuFlyout();
flyout.HoverBackgroundColor = SKColors.LightBlue;
Assert.Equal(SKColors.LightBlue, flyout.HoverBackgroundColor);
}
[Fact]
public void SeparatorColor_CanBeSet()
{
var flyout = new SkiaMenuFlyout();
flyout.SeparatorColor = SKColors.Gray;
Assert.Equal(SKColors.Gray, flyout.SeparatorColor);
}
[Fact]
public void ItemHeight_CanBeSet()
{
var flyout = new SkiaMenuFlyout();
flyout.ItemHeight = 32f;
Assert.Equal(32f, flyout.ItemHeight);
}
[Fact]
public void SeparatorHeight_CanBeSet()
{
var flyout = new SkiaMenuFlyout();
flyout.SeparatorHeight = 12f;
Assert.Equal(12f, flyout.SeparatorHeight);
}
[Fact]
public void MinWidth_CanBeSet()
{
var flyout = new SkiaMenuFlyout();
flyout.MinWidth = 200f;
Assert.Equal(200f, flyout.MinWidth);
}
[Fact]
public void ItemClickedEvent_CanBeSubscribed()
{
var flyout = new SkiaMenuFlyout();
MenuItem? clickedItem = null;
flyout.ItemClicked += (s, e) => clickedItem = e.Item;
Assert.Null(clickedItem);
}
}
public class MenuItemClickedEventArgsTests
{
[Fact]
public void Constructor_SetsItem()
{
var item = new MenuItem { Text = "Test" };
var args = new MenuItemClickedEventArgs(item);
Assert.Equal(item, args.Item);
}
}

View File

@@ -0,0 +1,160 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
using Xunit;
namespace Microsoft.Maui.Platform.Tests;
public class SkiaRefreshViewTests
{
[Fact]
public void Constructor_InitializesWithDefaultValues()
{
var refreshView = new SkiaRefreshView();
Assert.False(refreshView.IsRefreshing);
Assert.Null(refreshView.Content);
Assert.True(refreshView.IsEnabled);
}
[Fact]
public void IsRefreshing_CanBeSet()
{
var refreshView = new SkiaRefreshView();
refreshView.IsRefreshing = true;
Assert.True(refreshView.IsRefreshing);
}
[Fact]
public void Content_CanBeSet()
{
var refreshView = new SkiaRefreshView();
var content = new SkiaLabel { Text = "Test" };
refreshView.Content = content;
Assert.Equal(content, refreshView.Content);
}
[Fact]
public void Content_AddsChildToTree()
{
var refreshView = new SkiaRefreshView();
var content = new SkiaLabel { Text = "Test" };
refreshView.Content = content;
Assert.Equal(refreshView, content.Parent);
}
[Fact]
public void Content_RemovesPreviousChild()
{
var refreshView = new SkiaRefreshView();
var content1 = new SkiaLabel { Text = "First" };
var content2 = new SkiaLabel { Text = "Second" };
refreshView.Content = content1;
refreshView.Content = content2;
Assert.Null(content1.Parent);
Assert.Equal(refreshView, content2.Parent);
}
[Fact]
public void RefreshThreshold_DefaultValue()
{
var refreshView = new SkiaRefreshView();
Assert.Equal(80f, refreshView.RefreshThreshold);
}
[Fact]
public void RefreshThreshold_CanBeSet()
{
var refreshView = new SkiaRefreshView();
refreshView.RefreshThreshold = 100f;
Assert.Equal(100f, refreshView.RefreshThreshold);
}
[Fact]
public void RefreshColor_DefaultsToBlue()
{
var refreshView = new SkiaRefreshView();
Assert.Equal(new SKColor(33, 150, 243), refreshView.RefreshColor);
}
[Fact]
public void RefreshColor_CanBeSet()
{
var refreshView = new SkiaRefreshView();
refreshView.RefreshColor = SKColors.Red;
Assert.Equal(SKColors.Red, refreshView.RefreshColor);
}
[Fact]
public void RefreshBackgroundColor_DefaultsToWhite()
{
var refreshView = new SkiaRefreshView();
Assert.Equal(SKColors.White, refreshView.RefreshBackgroundColor);
}
[Fact]
public void RefreshBackgroundColor_CanBeSet()
{
var refreshView = new SkiaRefreshView();
refreshView.RefreshBackgroundColor = SKColors.LightGray;
Assert.Equal(SKColors.LightGray, refreshView.RefreshBackgroundColor);
}
[Fact]
public void RefreshingEvent_CanBeSubscribed()
{
var refreshView = new SkiaRefreshView();
bool eventRaised = false;
refreshView.Refreshing += (s, e) => eventRaised = true;
Assert.False(eventRaised); // Not raised yet
}
[Fact]
public void HitTest_ReturnsCorrectView()
{
var refreshView = new SkiaRefreshView();
var content = new SkiaLabel { Text = "Test" };
refreshView.Content = content;
refreshView.Arrange(new SKRect(0, 0, 200, 400));
var hit = refreshView.HitTest(100, 200);
Assert.NotNull(hit);
}
[Fact]
public void IsVisible_DefaultsToTrue()
{
var refreshView = new SkiaRefreshView();
Assert.True(refreshView.IsVisible);
}
[Fact]
public void IsEnabled_DefaultsToTrue()
{
var refreshView = new SkiaRefreshView();
Assert.True(refreshView.IsEnabled);
}
}

View File

@@ -0,0 +1,162 @@
// 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.Platform;
using SkiaSharp;
using Xunit;
namespace Microsoft.Maui.Controls.Linux.Tests.Views;
public class SkiaScrollViewTests
{
[Fact]
public void Constructor_SetsDefaultValues()
{
// Arrange & Act
var scrollView = new SkiaScrollView();
// Assert
scrollView.ScrollX.Should().Be(0);
scrollView.ScrollY.Should().Be(0);
scrollView.Content.Should().BeNull();
}
[Fact]
public void Content_WhenSet_UpdatesParent()
{
// Arrange
var scrollView = new SkiaScrollView();
var content = new SkiaStackLayout();
// Act
scrollView.Content = content;
// Assert
scrollView.Content.Should().Be(content);
content.Parent.Should().Be(scrollView);
}
[Fact]
public void ScrollY_WhenSet_ClampsToValidRange()
{
// Arrange
var scrollView = new SkiaScrollView();
var content = new SkiaStackLayout();
content.AddChild(new SkiaButton { Text = "1", RequestedHeight = 100 });
content.AddChild(new SkiaButton { Text = "2", RequestedHeight = 100 });
scrollView.Content = content;
scrollView.Measure(new SKSize(200, 100)); // Viewport smaller than content
scrollView.Arrange(new SKRect(0, 0, 200, 100));
// Act - Try to scroll below 0
scrollView.ScrollY = -50;
// Assert
scrollView.ScrollY.Should().BeGreaterOrEqualTo(0);
}
[Fact]
public void OnScroll_UpdatesScrollOffset()
{
// Arrange
var scrollView = new SkiaScrollView();
var content = new SkiaStackLayout();
for (int i = 0; i < 20; i++)
{
content.AddChild(new SkiaButton { Text = $"Button {i}", RequestedHeight = 50 });
}
scrollView.Content = content;
scrollView.Measure(new SKSize(200, 300));
scrollView.Arrange(new SKRect(0, 0, 200, 300));
var initialScrollY = scrollView.ScrollY;
// Act
scrollView.OnScroll(new ScrollEventArgs(100, 100, 0, 3)); // Scroll down
// Assert
scrollView.ScrollY.Should().BeGreaterThan(initialScrollY);
}
[Fact]
public void HitTest_ReturnsView()
{
// Arrange
var scrollView = new SkiaScrollView();
var content = new SkiaStackLayout();
var button = new SkiaButton { Text = "Test" };
content.AddChild(button);
scrollView.Content = content;
scrollView.Measure(new SKSize(200, 200));
scrollView.Arrange(new SKRect(0, 0, 200, 200));
// Act
var hit = scrollView.HitTest(100, 25);
// Assert - HitTest should return a view within bounds
hit.Should().NotBeNull();
}
[Fact]
public void ScrollY_CanBeSet()
{
// Arrange
var scrollView = new SkiaScrollView();
var content = new SkiaStackLayout();
for (int i = 0; i < 10; i++)
{
content.AddChild(new SkiaButton { Text = $"Button {i}" });
}
scrollView.Content = content;
scrollView.Measure(new SKSize(200, 100));
scrollView.Arrange(new SKRect(0, 0, 200, 100));
// Act
scrollView.ScrollY = 50;
// Assert - ScrollY should be settable
scrollView.ScrollY.Should().BeGreaterOrEqualTo(0);
}
[Fact]
public void Draw_DoesNotThrow()
{
// Arrange
var scrollView = new SkiaScrollView();
var content = new SkiaStackLayout();
content.AddChild(new SkiaButton { Text = "Test" });
scrollView.Content = content;
scrollView.Bounds = new SKRect(0, 0, 200, 200);
using var surface = SKSurface.Create(new SKImageInfo(300, 300));
var canvas = surface.Canvas;
// Act & Assert
var exception = Record.Exception(() => scrollView.Draw(canvas));
exception.Should().BeNull();
}
[Fact]
public void MaxScrollOffset_CalculatesCorrectly()
{
// Arrange
var scrollView = new SkiaScrollView();
var content = new SkiaStackLayout();
for (int i = 0; i < 20; i++)
{
content.AddChild(new SkiaButton { Text = $"Button {i}", RequestedHeight = 50 });
}
scrollView.Content = content;
scrollView.Measure(new SKSize(200, 200));
scrollView.Arrange(new SKRect(0, 0, 200, 200));
// Act - Scroll to maximum
scrollView.ScrollY = 10000; // Very large value
// Assert - Should be clamped to content height minus viewport
scrollView.ScrollY.Should().BeLessOrEqualTo(1000 - 200); // 20*50 - 200
}
}

View File

@@ -0,0 +1,160 @@
// 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.Platform;
using SkiaSharp;
using Xunit;
namespace Microsoft.Maui.Controls.Linux.Tests.Views;
public class SkiaSliderTests
{
[Fact]
public void Constructor_SetsDefaultValues()
{
// Arrange & Act
var slider = new SkiaSlider();
// Assert
slider.Value.Should().Be(0);
slider.Minimum.Should().Be(0);
slider.Maximum.Should().Be(100); // Default maximum is 100
slider.IsEnabled.Should().BeTrue();
}
[Fact]
public void Value_WhenSet_ClampsToRange()
{
// Arrange
var slider = new SkiaSlider { Minimum = 0, Maximum = 100 };
// Act & Assert - Below minimum
slider.Value = -10;
slider.Value.Should().Be(0);
// Act & Assert - Above maximum
slider.Value = 150;
slider.Value.Should().Be(100);
// Act & Assert - Within range
slider.Value = 50;
slider.Value.Should().Be(50);
}
[Fact]
public void Value_WhenChanged_RaisesValueChangedEvent()
{
// Arrange
var slider = new SkiaSlider { Minimum = 0, Maximum = 100 };
var eventRaised = false;
double newValue = 0;
slider.ValueChanged += (s, e) =>
{
eventRaised = true;
newValue = slider.Value;
};
// Act
slider.Value = 50;
// Assert
eventRaised.Should().BeTrue();
newValue.Should().Be(50);
}
[Fact]
public void Minimum_WhenSet_UpdatesProperty()
{
// Arrange
var slider = new SkiaSlider { Minimum = 0, Maximum = 100 };
// Act
slider.Minimum = 20;
// Assert
slider.Minimum.Should().Be(20);
}
[Fact]
public void Maximum_WhenSet_UpdatesProperty()
{
// Arrange
var slider = new SkiaSlider { Minimum = 0, Maximum = 100 };
// Act
slider.Maximum = 50;
// Assert
slider.Maximum.Should().Be(50);
}
[Fact]
public void IsEnabled_DefaultsToTrue()
{
// Arrange & Act
var slider = new SkiaSlider();
// Assert
slider.IsEnabled.Should().BeTrue();
}
[Fact]
public void ValueChanged_EventCanBeSubscribed()
{
// Arrange
var slider = new SkiaSlider { Minimum = 0, Maximum = 100 };
var eventCount = 0;
// Act
slider.ValueChanged += (s, e) => eventCount++;
slider.Value = 50;
slider.Value = 75;
// Assert
eventCount.Should().Be(2);
}
[Fact]
public void Draw_DoesNotThrow()
{
// Arrange
var slider = new SkiaSlider { Value = 50, Minimum = 0, Maximum = 100 };
slider.Bounds = new SKRect(0, 0, 200, 40);
using var surface = SKSurface.Create(new SKImageInfo(300, 100));
var canvas = surface.Canvas;
// Act & Assert
var exception = Record.Exception(() => slider.Draw(canvas));
exception.Should().BeNull();
}
[Fact]
public void ThumbColor_WhenSet_UpdatesProperty()
{
// Arrange
var slider = new SkiaSlider();
var color = new SKColor(255, 0, 0);
// Act
slider.ThumbColor = color;
// Assert
slider.ThumbColor.Should().Be(color);
}
[Fact]
public void TrackColor_WhenSet_UpdatesProperty()
{
// Arrange
var slider = new SkiaSlider();
var color = new SKColor(0, 255, 0);
// Act
slider.TrackColor = color;
// Assert
slider.TrackColor.Should().Be(color);
}
}

View File

@@ -0,0 +1,200 @@
// 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.Platform;
using SkiaSharp;
using Xunit;
using PlatformStackOrientation = Microsoft.Maui.Platform.StackOrientation;
namespace Microsoft.Maui.Controls.Linux.Tests.Views;
public class SkiaStackLayoutTests
{
[Fact]
public void Constructor_SetsDefaultValues()
{
// Arrange & Act
var layout = new SkiaStackLayout();
// Assert
layout.Orientation.Should().Be(PlatformStackOrientation.Vertical);
layout.Spacing.Should().Be(0);
layout.Children.Should().BeEmpty();
}
[Fact]
public void AddChild_AddsToChildren()
{
// Arrange
var layout = new SkiaStackLayout();
var button = new SkiaButton { Text = "Test" };
// Act
layout.AddChild(button);
// Assert
layout.Children.Should().Contain(button);
button.Parent.Should().Be(layout);
}
[Fact]
public void RemoveChild_RemovesFromChildren()
{
// Arrange
var layout = new SkiaStackLayout();
var button = new SkiaButton { Text = "Test" };
layout.AddChild(button);
// Act
layout.RemoveChild(button);
// Assert
layout.Children.Should().NotContain(button);
button.Parent.Should().BeNull();
}
[Fact]
public void Measure_Vertical_ReturnsPositiveSize()
{
// Arrange
var layout = new SkiaStackLayout
{
Orientation = PlatformStackOrientation.Vertical,
Spacing = 10
};
layout.AddChild(new SkiaButton { Text = "1" });
layout.AddChild(new SkiaButton { Text = "2" });
layout.AddChild(new SkiaButton { Text = "3" });
// Act
var size = layout.Measure(new SKSize(200, 1000));
// Assert - Size should account for 3 children with spacing
size.Height.Should().BeGreaterThan(0);
size.Width.Should().BeGreaterThan(0);
}
[Fact]
public void Measure_Horizontal_ReturnsPositiveSize()
{
// Arrange
var layout = new SkiaStackLayout
{
Orientation = PlatformStackOrientation.Horizontal,
Spacing = 10
};
layout.AddChild(new SkiaButton { Text = "1" });
layout.AddChild(new SkiaButton { Text = "2" });
layout.AddChild(new SkiaButton { Text = "3" });
// Act
var size = layout.Measure(new SKSize(1000, 200));
// Assert - Size should account for 3 children with spacing
size.Width.Should().BeGreaterThan(0);
size.Height.Should().BeGreaterThan(0);
}
[Fact]
public void Arrange_Vertical_PositionsChildren()
{
// Arrange
var layout = new SkiaStackLayout
{
Orientation = PlatformStackOrientation.Vertical,
Spacing = 10
};
var button1 = new SkiaButton { Text = "1" };
var button2 = new SkiaButton { Text = "2" };
layout.AddChild(button1);
layout.AddChild(button2);
// Act
layout.Measure(new SKSize(200, 500));
layout.Arrange(new SKRect(0, 0, 200, 500));
// Assert - Button2 should be below Button1
button2.Bounds.Top.Should().BeGreaterThan(button1.Bounds.Top);
}
[Fact]
public void Arrange_Horizontal_PositionsChildren()
{
// Arrange
var layout = new SkiaStackLayout
{
Orientation = PlatformStackOrientation.Horizontal,
Spacing = 10
};
var button1 = new SkiaButton { Text = "1" };
var button2 = new SkiaButton { Text = "2" };
layout.AddChild(button1);
layout.AddChild(button2);
// Act
layout.Measure(new SKSize(500, 200));
layout.Arrange(new SKRect(0, 0, 500, 200));
// Assert - Button2 should be to the right of Button1
button2.Bounds.Left.Should().BeGreaterThan(button1.Bounds.Left);
}
[Fact]
public void Padding_CanBeSet()
{
// Arrange
var layout = new SkiaStackLayout
{
Orientation = PlatformStackOrientation.Vertical,
Padding = new SKRect(20, 20, 20, 20)
};
var button = new SkiaButton { Text = "Test" };
layout.AddChild(button);
// Act
layout.Measure(new SKSize(300, 300));
layout.Arrange(new SKRect(0, 0, 300, 300));
// Assert - Padding property is set
layout.Padding.Left.Should().Be(20);
layout.Padding.Top.Should().Be(20);
}
[Fact]
public void Draw_DrawsAllChildren()
{
// Arrange
var layout = new SkiaStackLayout();
layout.AddChild(new SkiaButton { Text = "1" });
layout.AddChild(new SkiaButton { Text = "2" });
layout.Bounds = new SKRect(0, 0, 200, 200);
using var surface = SKSurface.Create(new SKImageInfo(300, 300));
var canvas = surface.Canvas;
// Act & Assert
var exception = Record.Exception(() => layout.Draw(canvas));
exception.Should().BeNull();
}
[Fact]
public void HitTest_ReturnsView()
{
// Arrange
var layout = new SkiaStackLayout { Orientation = PlatformStackOrientation.Vertical };
var button1 = new SkiaButton { Text = "1" };
var button2 = new SkiaButton { Text = "2" };
layout.AddChild(button1);
layout.AddChild(button2);
layout.Measure(new SKSize(200, 200));
layout.Arrange(new SKRect(0, 0, 200, 200));
// Act - Hit test within layout bounds
var hit = layout.HitTest(100, 10);
// Assert - Should return a view (either button1 or layout)
hit.Should().NotBeNull();
}
}

View File

@@ -0,0 +1,211 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
using Xunit;
namespace Microsoft.Maui.Platform.Tests;
public class SkiaSwipeViewTests
{
[Fact]
public void Constructor_InitializesWithDefaultValues()
{
var swipeView = new SkiaSwipeView();
Assert.Null(swipeView.Content);
Assert.Empty(swipeView.LeftItems);
Assert.Empty(swipeView.RightItems);
Assert.Empty(swipeView.TopItems);
Assert.Empty(swipeView.BottomItems);
Assert.Equal(SwipeMode.Reveal, swipeView.Mode);
}
[Fact]
public void Content_CanBeSet()
{
var swipeView = new SkiaSwipeView();
var content = new SkiaLabel { Text = "Swipeable" };
swipeView.Content = content;
Assert.Equal(content, swipeView.Content);
}
[Fact]
public void LeftItems_CanAddItems()
{
var swipeView = new SkiaSwipeView();
var item = new SwipeItem { Text = "Delete", BackgroundColor = SKColors.Red };
swipeView.LeftItems.Add(item);
Assert.Single(swipeView.LeftItems);
Assert.Equal("Delete", swipeView.LeftItems[0].Text);
}
[Fact]
public void RightItems_CanAddItems()
{
var swipeView = new SkiaSwipeView();
var item = new SwipeItem { Text = "Archive", BackgroundColor = SKColors.Blue };
swipeView.RightItems.Add(item);
Assert.Single(swipeView.RightItems);
}
[Fact]
public void SwipeItem_InvokedEvent_CanBeSubscribed()
{
var item = new SwipeItem { Text = "Test" };
bool invoked = false;
item.Invoked += (s, e) => invoked = true;
Assert.False(invoked); // Event not raised yet
}
[Fact]
public void Open_OpensSwipeInDirection()
{
var swipeView = new SkiaSwipeView();
swipeView.RightItems.Add(new SwipeItem { Text = "Delete" });
swipeView.Open(SwipeDirection.Left);
// Open state is internal, but we verify no exception
}
[Fact]
public void Close_ClosesOpenSwipe()
{
var swipeView = new SkiaSwipeView();
swipeView.LeftItems.Add(new SwipeItem { Text = "Test" });
swipeView.Open(SwipeDirection.Right);
swipeView.Close();
// Verifies no exception
}
[Fact]
public void Mode_CanBeSetToExecute()
{
var swipeView = new SkiaSwipeView();
swipeView.Mode = SwipeMode.Execute;
Assert.Equal(SwipeMode.Execute, swipeView.Mode);
}
[Fact]
public void LeftSwipeThreshold_CanBeSet()
{
var swipeView = new SkiaSwipeView();
swipeView.LeftSwipeThreshold = 150f;
Assert.Equal(150f, swipeView.LeftSwipeThreshold);
}
[Fact]
public void RightSwipeThreshold_CanBeSet()
{
var swipeView = new SkiaSwipeView();
swipeView.RightSwipeThreshold = 150f;
Assert.Equal(150f, swipeView.RightSwipeThreshold);
}
[Fact]
public void SwipeStartedEvent_CanBeSubscribed()
{
var swipeView = new SkiaSwipeView();
SwipeDirection? direction = null;
swipeView.SwipeStarted += (s, e) => direction = e.Direction;
// Simulate internal swipe start
swipeView.LeftItems.Add(new SwipeItem { Text = "Test" });
Assert.NotNull(swipeView);
}
[Fact]
public void SwipeEndedEvent_CanBeSubscribed()
{
var swipeView = new SkiaSwipeView();
bool ended = false;
swipeView.SwipeEnded += (s, e) => ended = true;
Assert.NotNull(swipeView);
}
[Fact]
public void SwipeItem_TextColor_CanBeSet()
{
var item = new SwipeItem { TextColor = SKColors.Yellow };
Assert.Equal(SKColors.Yellow, item.TextColor);
}
[Fact]
public void SwipeItem_BackgroundColor_CanBeSet()
{
var item = new SwipeItem { BackgroundColor = SKColors.Green };
Assert.Equal(SKColors.Green, item.BackgroundColor);
}
[Fact]
public void SwipeItem_IconSource_CanBeSet()
{
var item = new SwipeItem { IconSource = "delete.png" };
Assert.Equal("delete.png", item.IconSource);
}
[Fact]
public void TopItems_CanAddItems()
{
var swipeView = new SkiaSwipeView();
swipeView.TopItems.Add(new SwipeItem { Text = "Top" });
Assert.Single(swipeView.TopItems);
}
[Fact]
public void BottomItems_CanAddItems()
{
var swipeView = new SkiaSwipeView();
swipeView.BottomItems.Add(new SwipeItem { Text = "Bottom" });
Assert.Single(swipeView.BottomItems);
}
[Fact]
public void HitTest_ReturnsCorrectView()
{
var swipeView = new SkiaSwipeView();
swipeView.Content = new SkiaLabel { Text = "Content" };
swipeView.Arrange(new SKRect(0, 0, 300, 50));
var hit = swipeView.HitTest(150, 25);
Assert.NotNull(hit);
}
[Fact]
public void Measure_ReturnsCorrectSize()
{
var swipeView = new SkiaSwipeView();
swipeView.Content = new SkiaLabel { Text = "Test" };
var size = swipeView.Measure(new SKSize(300, 100));
Assert.True(size.Width <= 300);
Assert.True(size.Height <= 100);
}
}