diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 3d83b60100..e8f53d5791 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -138,6 +138,20 @@ if (something is not null)
* Please avoid adding new code that throws a `NotImplementedException`. According to the [Microsoft Docs](https://docs.microsoft.com/dotnet/api/system.notimplementedexception), we should only "throw a `NotImplementedException` exception in properties or methods in your own types when that member is still in development and will only later be implemented in production code. In other words, a NotImplementedException exception should be synonymous with 'still in development.'"
In other words, `NotImplementedException` implies that a feature is still in development, indicating that the Pull Request is incomplete.
+### ExpectedFailure Trait for Device Tests
+* Use `[Trait("Category", "ExpectedFailure")]` on device tests that are known to fail on certain platforms but are still under investigation. This allows CI to filter them out with `--filter "Category!=ExpectedFailure"` while keeping them runnable locally.
+* Do **not** use `[Fact(Skip = "...")]` for platform-specific failures — `Skip` hides the test entirely and it may silently rot. Prefer `[Trait("Category", "ExpectedFailure")]` so the test still runs locally and in unfiltered CI runs.
+* Example:
+```csharp
+[Fact]
+[Trait("Category", "ExpectedFailure")]
+public void Snackbar_Make_CreatesInstance()
+{
+ var snackbar = Snackbar.Make("Hello");
+ Assert.NotNull(snackbar);
+}
+```
+
### Bug Fixes
If you're looking for something to fix, please browse [open issues](https://github.com/CommunityToolkit/Maui/issues).
@@ -149,6 +163,290 @@ Follow the style used by the [.NET Foundation](https://github.com/dotnet/runtime
Read and follow our [Pull Request template](https://github.com/CommunityToolkit/Maui/blob/main/.github/PULL_REQUEST_TEMPLATE.md)
+## Device Testing (CommunityToolkit.Maui.DeviceTests)
+
+Device tests live in `src/CommunityToolkit.Maui.DeviceTests` and run inside a real MAUI app on Android, iOS, macOS Catalyst, and Windows. They verify platform-specific behavior (handler creation, platform service interactions, view measurement) that unit tests cannot.
+
+The project uses [DeviceRunners](https://github.com/mattleibow/DeviceRunners) by Matthew Leibowitz — a comprehensive cross-platform device testing framework that enables running tests on real devices across multiple platforms using various testing frameworks. DeviceRunners originated from migrating and modernizing the .NET MAUI team's device testing solutions.
+
+### DeviceRunners Architecture (how it maps to this project)
+
+DeviceRunners is a set of NuGet packages. This project consumes these packages — it does **not** need to build custom test runner infrastructure.
+
+| NuGet Package | Role in This Project |
+|---|---|
+| `DeviceRunners.Core` | Core abstractions: `ITestDiscoverer`, `ITestRunner`, `IResultChannelManager` |
+| `DeviceRunners.VisualRunners.Core` | Visual runner abstractions: result channels, formatters, test events |
+| `DeviceRunners.VisualRunners.Maui` | MAUI visual runner UI (pages, view models, diagnostics, app shell). Provides `UseVisualTestRunner()`. |
+| `DeviceRunners.VisualRunners.Xunit` | xUnit v2 test discovery and execution adapter. Provides `AddXunit()`. |
+| `DeviceRunners.Testing.Targets` | MSBuild targets enabling `dotnet test` for device projects (build → deploy → run → TRX). Provides `AddCliConfiguration()`. |
+
+Do **not** build custom `XunitFrontController` wrappers or `DeviceRunner` classes — DeviceRunners provides discovery, execution, visual runner UI, result collection, and `dotnet test` integration.
+
+### Platform Support
+
+```xml
+net10.0-android;net10.0-ios;net10.0-maccatalyst
+$(TargetFrameworks);net10.0-windows10.0.19041.0
+```
+
+The project uses conditional compilation for platform-specific code:
+
+```csharp
+#if ANDROID
+ // Android-specific code
+ return Android.App.Application.Context.CacheDir.AbsolutePath;
+#elif IOS || MACCATALYST
+ // iOS/macOS-specific code
+ var root = NSBundle.MainBundle.BundlePath;
+#elif WINDOWS
+ // Windows-specific code
+ return AppContext.BaseDirectory;
+#endif
+```
+
+### Project configuration
+
+**`MauiProgram.cs`** — register test assemblies and the test framework adapter:
+
+```csharp
+using DeviceRunners.VisualRunners;
+
+public static class MauiProgram
+{
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder
+ .UseVisualTestRunner(conf => conf
+ .AddCliConfiguration()
+ .AddConsoleResultChannel()
+ .AddTestAssembly(typeof(MauiProgram).Assembly)
+ .AddXunit())
+ .ConfigureFonts(fonts => { ... });
+ return builder.Build();
+ }
+}
+```
+
+- `AddCliConfiguration()` enables `dotnet test` support — reads env vars / CLI args for auto-start and TCP result streaming. When running interactively from the IDE (no env vars present), it is a no-op and the visual runner behaves normally.
+- `AddXunit()` registers the xUnit v2 test discoverer and runner. The project uses **xunit v2 (2.9.3)** — not xunit v3.
+- `AddTestAssembly(...)` tells the runner which assemblies contain tests.
+- `AddConsoleResultChannel()` writes pass/fail results to the console / trace log.
+- Do **not** call `builder.UseMauiApp()` — DeviceRunners registers its own `VisualRunnerApp` via `UseVisualTestRunner`.
+
+**`.csproj`** — required packages:
+
+```xml
+
+
+
+
+
+```
+
+Also required: `false` because the MAUI app defines its own `Program.Main`. No custom NuGet feeds are needed — all packages are on NuGet.org.
+
+### Writing device tests
+
+Device test classes are plain xUnit `[Fact]`/`[Theory]` classes in `Tests/` organized by area (`Behaviors/`, `Converters/`, `Views/`, `Core/`, `Camera/`, `Maps/`, `MediaElement/`, etc.):
+
+```csharp
+namespace CommunityToolkit.Maui.DeviceTests.Tests.Behaviors;
+
+public class MyBehaviorTests
+{
+ [Fact]
+ public void MyBehavior_DefaultValue_IsCorrect()
+ {
+ var behavior = new MyBehavior();
+ Assert.Equal(expectedValue, behavior.SomeProperty);
+ }
+}
+```
+
+#### Main thread requirement
+
+**Android requires all view hierarchy operations on the main thread.** Tests that construct platform behaviors (e.g., `StatusBarBehavior`), create `Page` instances, add behaviors to pages, or call `element.ToHandler(context)` **must** run on the main thread via `MainThread.InvokeOnMainThreadAsync`:
+
+```csharp
+[Fact]
+public async Task Behavior_CanBeAttachedToPage()
+{
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ var page = new ContentPage();
+ var behavior = new StatusBarBehavior { StatusBarColor = Colors.Fuchsia };
+ page.Behaviors.Add(behavior);
+ Assert.Single(page.Behaviors.OfType());
+ });
+}
+```
+
+Tests that only set/get CLR properties (no platform view interaction) do **not** need main-thread dispatch:
+
+```csharp
+[Fact]
+public void Behavior_DefaultColor_IsTransparent()
+{
+ var behavior = new StatusBarBehavior();
+ Assert.Equal(Colors.Transparent, behavior.StatusBarColor); // no platform touch
+}
+```
+
+#### Handler creation
+
+Create handlers directly via `element.ToHandler(context)` using the app's `MauiContext`. Do **not** replace `window.Page` to force handler creation — DeviceRunners manages its own page:
+
+```csharp
+[Fact]
+public async Task LabelHandlerIsCreated()
+{
+ var label = new Label { Text = "Test" };
+ var handler = await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ var context = Application.Current?.Handler?.MauiContext;
+ Assert.NotNull(context);
+ return label.ToHandler(context);
+ });
+ Assert.NotNull(handler.PlatformView);
+}
+```
+
+### Running tests
+
+**Visual runner (IDE / interactive debugging):**
+```bash
+dotnet build src/CommunityToolkit.Maui.DeviceTests/CommunityToolkit.Maui.DeviceTests.csproj -f net10.0-android -t:Run
+```
+Launches the app with the DeviceRunners visual runner UI showing pass/fail counts, per-test details, and diagnostics.
+
+**`dotnet test` (CI / headless, recommended):**
+```bash
+dotnet test src/CommunityToolkit.Maui.DeviceTests/CommunityToolkit.Maui.DeviceTests.csproj -f net10.0-android
+dotnet test ... -f net10.0-android --filter "FullyQualifiedName~StatusBarBehavior"
+```
+`DeviceRunners.Testing.Targets` hooks into `dotnet test` to build, deploy, run, and collect TRX results automatically. Filter with standard `--filter` syntax.
+
+How `dotnet test` works under the hood:
+1. **Build** — The app is compiled for the target platform (APK, .app bundle, .exe)
+2. **Deploy** — The DeviceRunners CLI tool installs the app on the device/simulator
+3. **Launch** — The app starts with configuration (env vars or CLI args) that tells it to auto-run tests and connect back via TCP
+4. **Collect** — The CLI listens on a TCP port for NDJSON test events and writes a TRX file
+5. **Report** — Results are parsed and reported in the standard `dotnet test` format
+
+MSBuild properties for configuration (set via `-p:` or in `.csproj`):
+
+| Property | Default | Description |
+|---|---|---|
+| `DeviceRunnersPort` | 16384 | TCP port for test result collection |
+| `DeviceRunnersConnectionTimeout` | 120 | Seconds to wait for the app to connect |
+| `DeviceRunnersDevice` | (auto) | Target device ID |
+
+### Platform-specific test files
+
+DeviceTests is **not** a NuGet-packaged project, so standard `*.cs` naming is used. For platform-specific tests, use `#if` directives:
+
+```csharp
+#if ANDROID
+[Fact]
+public void PlatformIsAndroid() => Assert.True(OperatingSystem.IsAndroid());
+#elif IOS
+[Fact]
+public void PlatformIsIOS() => Assert.True(OperatingSystem.IsIOS());
+#elif MACCATALYST
+[Fact]
+public void PlatformIsMacCatalyst() => Assert.True(OperatingSystem.IsMacCatalyst());
+#elif WINDOWS
+[Fact]
+public void PlatformIsWindows() => Assert.True(OperatingSystem.IsWindows());
+#endif
+```
+
+### Key DeviceRunners Interfaces
+
+These are provided by DeviceRunners — tests can reference them when extending the runner:
+
+```csharp
+// Core testing interfaces (in DeviceRunners.Core)
+public interface ITestDiscoverer
+{
+ Task> DiscoverTestsAsync(IEnumerable sources);
+}
+
+public interface ITestRunner
+{
+ Task RunTestsAsync(IEnumerable testCases);
+}
+
+public interface IResultChannelManager
+{
+ Task SendResultsAsync(TestResult result);
+}
+
+// Platform abstractions (in DeviceRunners.VisualRunners.Core)
+public interface IAppTerminator
+{
+ Task TerminateAsync();
+}
+
+public interface IDiagnosticsManager
+{
+ Task CollectDiagnosticsAsync();
+}
+```
+
+### Common Scenarios for Copilot Assistance
+
+#### When adding new device tests
+- Create a plain xUnit `[Fact]`/`[Theory]` class in `Tests/{Area}/`
+- If the test touches platform views/behaviors, use `async Task` + `MainThread.InvokeOnMainThreadAsync`
+- If the test only checks CLR properties, use synchronous `void`
+- For handler creation, use `element.ToHandler(context)` with `Application.Current?.Handler?.MauiContext`
+
+#### When adding platform-specific tests
+- Use `#if ANDROID` / `#elif IOS` / `#elif MACCATALYST` / `#elif WINDOWS` directives
+- Use `#if` guards at the method level, not the class level
+- Standard `*.cs` naming (DeviceTests is not a NuGet-packaged project)
+
+#### When debugging test failures
+- Android: Check `adb logcat` for `[DeviceRunners]` / `[FAIL]` trace messages
+- The visual runner's Diagnostics page shows assembly paths, environment, and runner logs
+- Use `--filter "FullyQualifiedName~TestName"` with `dotnet test` to isolate a single test
+
+#### When working with MAUI integration
+- Use MAUI service registration patterns via `builder.Services`
+- Follow MAUI lifecycle management (tests run after app startup)
+- Test across all target platforms before submitting
+
+### Dos and Don'ts
+
+**Do:**
+- Use `MainThread.InvokeOnMainThreadAsync` for any test that constructs or touches platform views/behaviors
+- Use `element.ToHandler(context)` for handler creation
+- Use `Trace.WriteLine()` (not `Debug.WriteLine()`) for logging
+- Add tests in `Tests/` organized by area (`Behaviors/`, `Converters/`, `Views/`, etc.)
+- Use `xunit` v2 `[Fact]` and `[Theory]` attributes
+- Use `dotnet test` for CI/headless runs; use `dotnet build -t:Run` for interactive debugging
+
+**Don't:**
+- Don't build custom `DeviceRunner` or `XunitFrontController` wrappers — DeviceRunners handles this
+- Don't call `builder.UseMauiApp()` in `MauiProgram.cs` — DeviceRunners registers its own `VisualRunnerApp`
+- Don't replace `window.Page` to force handler creation
+- Don't use `xunit.runner.utility` directly — DeviceRunners manages test execution
+- Don't add `NuGet.config` with the `dotnet-eng` feed — DeviceRunners packages are on NuGet.org
+- Don't use `NotImplementedException` — implement the feature or use `NotSupportedException`
+
+### Historical Context
+
+DeviceRunners consolidates and modernizes several earlier testing solutions:
+- **xunit/devices.xunit** — Migrated to .NET MAUI with separated UI components
+- **xunit/uitest.xunit** — Migrated to .NET MAUI
+- **nunit/nunit.xamarin** — Migrated to .NET MAUI with individual test support
+- **dotnet/maui** — Temporary hosting during migration; the .NET MAUI team now recommends DeviceRunners for device testing
+
+The architecture reflects lessons learned from these migrations, emphasizing modularity, cross-platform support, and separation of concerns between test execution and user interfaces. This project adopts DeviceRunners as its device testing infrastructure, replacing the earlier custom `XunitFrontController`-based runner.
+
## Submitting Contributions
1. Fork the repository and create a new branch for your changes.
2. Implement your changes using GitHub Copilot as needed.
diff --git a/.github/instructions/device-tests.instructions.md b/.github/instructions/device-tests.instructions.md
new file mode 100644
index 0000000000..dc32bd2770
--- /dev/null
+++ b/.github/instructions/device-tests.instructions.md
@@ -0,0 +1,69 @@
+---
+description: 'Building and running device tests for the .NET MAUI Community Toolkit: the DeviceRunners test runner, handler creation without page navigation, and avoiding infinite re-run loops'
+applyTo: 'src/CommunityToolkit.Maui.DeviceTests/**/*.cs'
+---
+
+## Device Tests
+
+Device tests live in `src/CommunityToolkit.Maui.DeviceTests` and run inside a real MAUI application on a target platform (Windows, Android, iOS, MacCatalyst). They verify platform-specific behavior that unit tests cannot.
+
+### Test runner architecture
+
+The project uses [DeviceRunners](https://github.com/mattleibow/DeviceRunners) by Matthew Leibowitz — the same infrastructure recommended by the .NET MAUI team:
+
+- **`DeviceRunners.VisualRunners.Maui`** provides the visual runner UI, pages, view models, and diagnostics.
+- **`DeviceRunners.VisualRunners.Xunit`** provides xUnit v2 test discovery and execution.
+- **`DeviceRunners.Testing.Targets`** enables `dotnet test` for device projects (TRX results, filtering, CI integration).
+- Tests are registered via `builder.UseVisualTestRunner(conf => conf.AddTestAssembly(...).AddXunit())` in `MauiProgram.cs`.
+
+Do **not** build a custom `DeviceRunner`/`XunitFrontController` wrapper — DeviceRunners handles discovery, execution, result collection, and diagnostics.
+
+### Creating handlers in tests
+
+When a test needs a handler/platform view, create it directly with `element.ToHandler(context)` using the application's `MauiContext` — do **not** replace `window.Page` to force handler creation:
+
+```csharp
+var context = Application.Current?.Handler?.MauiContext;
+var handler = element.ToHandler(context);
+```
+
+Handler creation must run on the main thread (`MainThread.InvokeOnMainThreadAsync`).
+
+### Platform behavior tests
+
+Tests that construct platform behaviors (e.g., `StatusBarBehavior`) or add behaviors to a `Page` must also run on the main thread, because Android requires all view hierarchy operations on the main thread:
+
+```csharp
+[Fact]
+public async Task StatusBarBehavior_CanBeAttachedToPage()
+{
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ var page = new ContentPage();
+ var behavior = new StatusBarBehavior { StatusBarColor = Colors.Fuchsia };
+ page.Behaviors.Add(behavior);
+ Assert.Single(page.Behaviors.OfType());
+ });
+}
+```
+
+### Running
+
+**Visual Runner (IDE / Interactive):**
+```bash
+dotnet build src/CommunityToolkit.Maui.DeviceTests/CommunityToolkit.Maui.DeviceTests.csproj -f net10.0-android -t:Run
+```
+
+**`dotnet test` (CI / Headless, Recommended):**
+```bash
+dotnet test src/CommunityToolkit.Maui.DeviceTests/CommunityToolkit.Maui.DeviceTests.csproj -f net10.0-android
+dotnet test ... --filter "FullyQualifiedName~StatusBarBehavior"
+```
+
+The `DeviceRunners.Testing.Targets` package hooks into `dotnet test` to build, deploy, run, and collect TRX results automatically.
+
+### Packages
+
+- xunit **v2** (`2.9.3`) is required.
+- DeviceRunners packages are from NuGet.org (`DeviceRunners.VisualRunners.Maui`, `DeviceRunners.VisualRunners.Xunit`, `DeviceRunners.Testing.Targets` at `0.1.0-preview.12`).
+- No custom NuGet feeds are needed — the `NuGet.config` for `dotnet-eng` has been removed.
diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml
index 16f36e676e..0baf905d42 100644
--- a/.github/workflows/dotnet-build.yml
+++ b/.github/workflows/dotnet-build.yml
@@ -46,6 +46,7 @@ env:
PathToCommunityToolkitMediaElementSourceGeneratorsUnitTestDirectory: 'src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests'
PathToCommunityToolkitSourceGeneratorsUnitTestCsproj: 'src/CommunityToolkit.Maui.SourceGenerators.UnitTests/CommunityToolkit.Maui.SourceGenerators.UnitTests.csproj'
PathToCommunityToolkitAnalyzersBenchmarkCsproj: 'src/CommunityToolkit.Maui.Analyzers.Benchmarks/CommunityToolkit.Maui.Analyzers.Benchmarks.csproj'
+ PathToCommunityToolkitDeviceTestsCsproj: 'src/CommunityToolkit.Maui.DeviceTests/CommunityToolkit.Maui.DeviceTests.csproj'
CommunityToolkitLibrary_Xcode_Version: '26.1'
CommunityToolkitSample_Xcode_Version: '^26'
@@ -255,7 +256,7 @@ jobs:
run: dotnet pack -c Release ${{ env.PathToCommunityToolkitMapsCsproj }} -p:PackageVersion=${{ env.NugetPackageVersionMaps }} -p:Version=${{ env.NugetPackageVersionMaps }}
- name: Copy NuGet Packages to Staging Directory
- if: ${{ runner.os == 'Windows' }} && !startsWith(github.ref, 'refs/tags/')
+ if: ${{ runner.os == 'Windows' && !startsWith(github.ref, 'refs/tags/') }}
run: |
mkdir -p ${{ github.workspace }}/nuget
Get-ChildItem -Path "./src" -Recurse | Where-Object { $_.Extension -match "nupkg" } | Copy-Item -Destination "${{ github.workspace }}/nuget"
@@ -277,6 +278,151 @@ jobs:
name: packages
path: ${{ github.workspace }}/nuget/
+ device_test_windows:
+ name: Run Device Tests (Windows)
+ runs-on: windows-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - name: Install .NET SDK v${{ env.TOOLKIT_NET_VERSION }}
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
+ with:
+ dotnet-version: ${{ env.TOOLKIT_NET_VERSION }}
+ dotnet-quality: 'ga'
+
+ - name: Install .NET MAUI Workload
+ run: |
+ dotnet workload install maui
+ dotnet workload update
+
+ - name: Display dotnet info
+ run: dotnet --info
+
+ - name: Enable Windows Developer Mode
+ run: |
+ reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /v AllowDevelopmentWithoutDevLicense /t REG_DWORD /d 1 /f
+ reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /v AllowAllTrustedApps /t REG_DWORD /d 1 /f
+ shell: pwsh
+
+ - name: Restore test project
+ run: dotnet restore ${{ env.PathToCommunityToolkitDeviceTestsCsproj }}
+
+ - name: Install Windows App Runtime framework
+ shell: pwsh
+ run: |
+ $packagesRoot = Join-Path $env:USERPROFILE ".nuget/packages"
+ $wasdkPackages = Get-ChildItem $packagesRoot -Directory |
+ Where-Object { $_.Name -like 'microsoft.windowsappsdk*' } |
+ Sort-Object Name -Descending
+
+ if (-not $wasdkPackages) {
+ Write-Error "Windows App SDK NuGet package not found"
+ exit 1
+ }
+
+ $msixFiles = foreach ($package in $wasdkPackages) {
+ Get-ChildItem $package.FullName -Recurse -Filter '*.msix' -ErrorAction SilentlyContinue |
+ Where-Object { $_.FullName -match 'tools[\\/]+MSIX[\\/]+.*\.msix$' }
+ }
+
+ $msixFiles = $msixFiles | Sort-Object FullName -Unique
+ if (-not $msixFiles) {
+ Write-Error "Windows App SDK MSIX runtime files were not found"
+ exit 1
+ }
+
+ $msixFiles | ForEach-Object {
+ Write-Host "Installing: $($_.FullName)"
+ Add-AppxPackage -Path $_.FullName -ErrorAction SilentlyContinue
+ }
+
+ - name: Run Tests
+ run: |
+ dotnet test ${{ env.PathToCommunityToolkitDeviceTestsCsproj }} `
+ -f net10.0-windows10.0.19041.0 `
+ -c Release `
+ -p:IsCI=true `
+ --filter "Category!=ExpectedFailure" `
+ --logger "trx;LogFileName=test-results.trx" `
+ --results-directory .\artifacts\test-results `
+ /bl:.\artifacts\logs\msbuild-test.binlog
+ shell: pwsh
+
+ - name: Clean up build artifacts before upload
+ if: always()
+ shell: pwsh
+ run: |
+ Remove-Item -Recurse -Force .\artifacts\bin -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force .\artifacts\obj -ErrorAction SilentlyContinue
+
+ - name: Upload Test Results
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: Test Results (dotnet test) - Windows
+ path: .\artifacts
+
+ device_test_maccatalyst:
+ name: Run Device Tests (macOS Catalyst)
+ runs-on: macos-26
+ env:
+ CommunityToolkitLibrary_Xcode_Version: '26.6'
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - name: Set Xcode Version
+ uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
+ with:
+ xcode-version: '26.6'
+
+ - name: Set Xcode Version
+ uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
+ with:
+ xcode-version: ${{ env.CommunityToolkitLibrary_Xcode_Version }}
+
+ - name: Install .NET SDK v${{ env.TOOLKIT_NET_VERSION }}
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
+ with:
+ dotnet-version: ${{ env.TOOLKIT_NET_VERSION }}
+ dotnet-quality: 'ga'
+
+ - name: Install .NET MAUI Workload
+ run: |
+ dotnet workload install maui
+ dotnet workload update
+
+ - name: Display dotnet info
+ run: dotnet --info
+
+ - name: Run Tests
+ run: |
+ dotnet test ${{ env.PathToCommunityToolkitDeviceTestsCsproj }} \
+ -f net10.0-maccatalyst \
+ -r maccatalyst-x64 \
+ -c Release \
+ -p:IsCI=true \
+ --filter "Category!=ExpectedFailure" \
+ --logger "trx;LogFileName=test-results.trx" \
+ --results-directory ./artifacts/test-results \
+ /bl:./artifacts/logs/msbuild-test.binlog
+ shell: bash
+
+ - name: Clean up build artifacts before upload
+ if: always()
+ shell: bash
+ run: |
+ rm -rf ./artifacts/bin
+ rm -rf ./artifacts/obj
+
+ - name: Upload Test Results
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: Test Results (dotnet test) - macOS Catalyst
+ path: ./artifacts
+
sign:
needs: [ build_library ]
if: ${{ startsWith(github.ref, 'refs/tags/') || (github.event_name != 'pull_request') }}
diff --git a/.gitignore b/.gitignore
index 10868dd944..bba15b0eb9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -267,4 +267,6 @@ paket-files/
/samples/workload-install.ps1
# Benchmarkdot
-BenchmarkDotNet.Artifacts/
\ No newline at end of file
+BenchmarkDotNet.Artifacts/
+/src/CommunityToolkit.Maui.DeviceTests/test-results
+/CLAUDE.md
diff --git a/Directory.Build.props b/Directory.Build.props
index dc738d6214..6c99229f8f 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -216,10 +216,9 @@ https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitati
- true
true
diff --git a/samples/CommunityToolkit.Maui.Sample.slnx b/samples/CommunityToolkit.Maui.Sample.slnx
index aa531a4383..042a96642a 100644
--- a/samples/CommunityToolkit.Maui.Sample.slnx
+++ b/samples/CommunityToolkit.Maui.Sample.slnx
@@ -1,20 +1,74 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -27,16 +81,67 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Maui.Camera/CommunityToolkit.Maui.Camera.csproj b/src/CommunityToolkit.Maui.Camera/CommunityToolkit.Maui.Camera.csproj
index e64ff790aa..58a9522cb9 100644
--- a/src/CommunityToolkit.Maui.Camera/CommunityToolkit.Maui.Camera.csproj
+++ b/src/CommunityToolkit.Maui.Camera/CommunityToolkit.Maui.Camera.csproj
@@ -51,6 +51,7 @@
+
diff --git a/src/CommunityToolkit.Maui.Core/CommunityToolkit.Maui.Core.csproj b/src/CommunityToolkit.Maui.Core/CommunityToolkit.Maui.Core.csproj
index 320813d786..a449143ef2 100644
--- a/src/CommunityToolkit.Maui.Core/CommunityToolkit.Maui.Core.csproj
+++ b/src/CommunityToolkit.Maui.Core/CommunityToolkit.Maui.Core.csproj
@@ -49,6 +49,7 @@
+
diff --git a/src/CommunityToolkit.Maui.DeviceTests/CommunityToolkit.Maui.DeviceTests.csproj b/src/CommunityToolkit.Maui.DeviceTests/CommunityToolkit.Maui.DeviceTests.csproj
new file mode 100644
index 0000000000..ca9cc3504d
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/CommunityToolkit.Maui.DeviceTests.csproj
@@ -0,0 +1,73 @@
+
+
+
+ $(NetVersion)-android;$(NetVersion)-ios;$(NetVersion)-maccatalyst
+ $(TargetFrameworks);$(NetVersion)-windows10.0.19041.0
+ Exe
+ true
+ true
+ true
+ CommunityToolkit.Maui.DeviceTests
+
+
+ CommunityToolkit.Maui Device Tests
+
+
+ com.microsoft.CommunityToolkit.Maui.DeviceTests
+ A1B2C3D4-E5F6-7890-ABCD-EF1234567890
+
+
+ 1.0
+ 1
+
+
+ MSIX
+ true
+
+
+ false
+
+
+
+ $(NoWarn);xUnit2032;xUnit1004;IL3000
+
+ 15.0
+ 15.0
+ 21.0
+ 10.0.17763.0
+ 10.0.17763.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Maui.DeviceTests/GlobalUsings.cs b/src/CommunityToolkit.Maui.DeviceTests/GlobalUsings.cs
new file mode 100644
index 0000000000..2b0e32bec2
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/GlobalUsings.cs
@@ -0,0 +1,3 @@
+global using Xunit;
+
+[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true, MaxParallelThreads = 1)]
diff --git a/src/CommunityToolkit.Maui.DeviceTests/HandlerTests.cs b/src/CommunityToolkit.Maui.DeviceTests/HandlerTests.cs
new file mode 100644
index 0000000000..adc4d1570e
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/HandlerTests.cs
@@ -0,0 +1,71 @@
+using Microsoft.Maui.Platform;
+using Xunit;
+
+namespace CommunityToolkit.Maui.DeviceTests;
+
+public class HandlerTests
+{
+ ///
+ /// Creates a handler for the given element using the application's .
+ /// This creates the handler and its platform view directly, without navigating away from or
+ /// replacing the current page (so the visual test runner page stays visible).
+ /// Must run on the main thread: on Windows/Android, handler and platform-view creation
+ /// requires the UI thread.
+ ///
+ static IPlatformViewHandler CreateHandler(IElement element)
+ {
+ var context = Application.Current?.Handler?.MauiContext;
+ Assert.NotNull(context);
+
+ var handler = element.ToHandler(context);
+ return Assert.IsAssignableFrom(handler);
+ }
+
+ [Fact]
+ public async Task LabelHandlerIsCreated()
+ {
+ var label = new Label { Text = "Test" };
+ var handler = await MainThread.InvokeOnMainThreadAsync(() => CreateHandler(label));
+
+ Assert.NotNull(handler);
+ Assert.NotNull(handler.PlatformView);
+ }
+
+ [Fact]
+ public async Task ButtonHandlerIsCreated()
+ {
+ var button = new Button { Text = "Click Me" };
+ var handler = await MainThread.InvokeOnMainThreadAsync(() => CreateHandler(button));
+
+ Assert.NotNull(handler);
+ Assert.NotNull(handler.PlatformView);
+ }
+
+ [Fact]
+ public async Task EntryHandlerIsCreated()
+ {
+ var entry = new Entry { Text = "Hello" };
+ var handler = await MainThread.InvokeOnMainThreadAsync(() => CreateHandler(entry));
+
+ Assert.NotNull(handler);
+ Assert.NotNull(handler.PlatformView);
+ }
+
+ [Fact]
+ public async Task StackLayoutHandlerIsCreated()
+ {
+ var layout = new VerticalStackLayout
+ {
+ Children =
+ {
+ new Label { Text = "Child 1" },
+ new Label { Text = "Child 2" }
+ }
+ };
+
+ var handler = await MainThread.InvokeOnMainThreadAsync(() => CreateHandler(layout));
+
+ Assert.NotNull(handler);
+ Assert.NotNull(handler.PlatformView);
+ }
+}
diff --git a/src/CommunityToolkit.Maui.DeviceTests/MauiProgram.cs b/src/CommunityToolkit.Maui.DeviceTests/MauiProgram.cs
new file mode 100644
index 0000000000..826b10389b
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/MauiProgram.cs
@@ -0,0 +1,24 @@
+using DeviceRunners.VisualRunners;
+
+namespace CommunityToolkit.Maui.DeviceTests;
+
+public static class MauiProgram
+{
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+
+ builder
+ .UseVisualTestRunner(conf => conf
+ .AddCliConfiguration()
+ .AddConsoleResultChannel()
+ .AddTestAssembly(typeof(MauiProgram).Assembly)
+ .AddXunit())
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ });
+
+ return builder.Build();
+ }
+}
diff --git a/src/CommunityToolkit.Maui.DeviceTests/PlatformDetectionTests.cs b/src/CommunityToolkit.Maui.DeviceTests/PlatformDetectionTests.cs
new file mode 100644
index 0000000000..e1361ee914
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/PlatformDetectionTests.cs
@@ -0,0 +1,55 @@
+using Xunit;
+
+namespace CommunityToolkit.Maui.DeviceTests;
+
+public class PlatformDetectionTests
+{
+ [Fact]
+ public void DevicePlatformIsNotUnknown()
+ {
+ Assert.NotEqual(DevicePlatform.Unknown, DeviceInfo.Platform);
+ }
+
+ [Fact]
+ [Trait("Category", "ExpectedFailure")]
+ public void DeviceIdiomIsNotUnknown()
+ {
+ Assert.NotEqual(DeviceIdiom.Unknown, DeviceInfo.Idiom);
+ }
+
+ [Fact]
+ public void OperatingSystemVersionIsPopulated()
+ {
+ Assert.False(string.IsNullOrWhiteSpace(DeviceInfo.VersionString));
+ }
+
+#if ANDROID
+ [Fact]
+ public void PlatformIsAndroid()
+ {
+ Assert.Equal(DevicePlatform.Android, DeviceInfo.Platform);
+ Assert.True(OperatingSystem.IsAndroid());
+ }
+#elif IOS
+ [Fact]
+ public void PlatformIsIOS()
+ {
+ Assert.Equal(DevicePlatform.iOS, DeviceInfo.Platform);
+ Assert.True(OperatingSystem.IsIOS());
+ }
+#elif MACCATALYST
+ [Fact]
+ public void PlatformIsMacCatalyst()
+ {
+ Assert.Equal(DevicePlatform.MacCatalyst, DeviceInfo.Platform);
+ Assert.True(OperatingSystem.IsMacCatalyst());
+ }
+#elif WINDOWS
+ [Fact]
+ public void PlatformIsWindows()
+ {
+ Assert.Equal(DevicePlatform.WinUI, DeviceInfo.Platform);
+ Assert.True(OperatingSystem.IsWindows());
+ }
+#endif
+}
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Platforms/Android/AndroidManifest.xml b/src/CommunityToolkit.Maui.DeviceTests/Platforms/Android/AndroidManifest.xml
new file mode 100644
index 0000000000..41b86bc6b7
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Platforms/Android/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Platforms/Android/MainActivity.cs b/src/CommunityToolkit.Maui.DeviceTests/Platforms/Android/MainActivity.cs
new file mode 100644
index 0000000000..f81bc801f3
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Platforms/Android/MainActivity.cs
@@ -0,0 +1,9 @@
+using Android.App;
+using Android.Content.PM;
+
+namespace CommunityToolkit.Maui.DeviceTests;
+
+[Activity(Theme = "@style/Maui.SplashTheme", ResizeableActivity = true, MainLauncher = true, LaunchMode = LaunchMode.SingleTask, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize)]
+public class MainActivity : MauiAppCompatActivity
+{
+}
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Platforms/Android/MainApplication.cs b/src/CommunityToolkit.Maui.DeviceTests/Platforms/Android/MainApplication.cs
new file mode 100644
index 0000000000..e9b0ec5101
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Platforms/Android/MainApplication.cs
@@ -0,0 +1,15 @@
+using Android.App;
+using Android.Runtime;
+
+namespace CommunityToolkit.Maui.DeviceTests;
+
+[Application]
+public class MainApplication : MauiApplication
+{
+ public MainApplication(IntPtr handle, JniHandleOwnership ownership)
+ : base(handle, ownership)
+ {
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Platforms/MacCatalyst/AppDelegate.cs b/src/CommunityToolkit.Maui.DeviceTests/Platforms/MacCatalyst/AppDelegate.cs
new file mode 100644
index 0000000000..da7232d04c
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Platforms/MacCatalyst/AppDelegate.cs
@@ -0,0 +1,9 @@
+using Foundation;
+
+namespace CommunityToolkit.Maui.DeviceTests;
+
+[Register(nameof(AppDelegate))]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Platforms/MacCatalyst/Info.plist b/src/CommunityToolkit.Maui.DeviceTests/Platforms/MacCatalyst/Info.plist
new file mode 100644
index 0000000000..c96dd0a225
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Platforms/MacCatalyst/Info.plist
@@ -0,0 +1,30 @@
+
+
+
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+
+
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Platforms/MacCatalyst/Program.cs b/src/CommunityToolkit.Maui.DeviceTests/Platforms/MacCatalyst/Program.cs
new file mode 100644
index 0000000000..12bbbfd3d4
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Platforms/MacCatalyst/Program.cs
@@ -0,0 +1,8 @@
+using UIKit;
+
+namespace CommunityToolkit.Maui.DeviceTests;
+
+public class Program
+{
+ static void Main(string[] args) => UIApplication.Main(args, null, typeof(AppDelegate));
+}
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Platforms/Windows/App.xaml b/src/CommunityToolkit.Maui.DeviceTests/Platforms/Windows/App.xaml
new file mode 100644
index 0000000000..a8b0e3bb76
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Platforms/Windows/App.xaml
@@ -0,0 +1,6 @@
+
+
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Platforms/Windows/App.xaml.cs b/src/CommunityToolkit.Maui.DeviceTests/Platforms/Windows/App.xaml.cs
new file mode 100644
index 0000000000..62a557f57f
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Platforms/Windows/App.xaml.cs
@@ -0,0 +1,18 @@
+namespace CommunityToolkit.Maui.DeviceTests.Windows;
+
+///
+/// Provides application-specific behavior to supplement the default Application class.
+///
+public partial class App : MauiWinUIApplication
+{
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ InitializeComponent();
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Platforms/Windows/Package.appxmanifest b/src/CommunityToolkit.Maui.DeviceTests/Platforms/Windows/Package.appxmanifest
new file mode 100644
index 0000000000..cf4e7f9170
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Platforms/Windows/Package.appxmanifest
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+ $placeholder$
+ Microsoft
+ $placeholder$.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Platforms/Windows/app.manifest b/src/CommunityToolkit.Maui.DeviceTests/Platforms/Windows/app.manifest
new file mode 100644
index 0000000000..48a74c27c3
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Platforms/Windows/app.manifest
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Platforms/iOS/AppDelegate.cs b/src/CommunityToolkit.Maui.DeviceTests/Platforms/iOS/AppDelegate.cs
new file mode 100644
index 0000000000..da7232d04c
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Platforms/iOS/AppDelegate.cs
@@ -0,0 +1,9 @@
+using Foundation;
+
+namespace CommunityToolkit.Maui.DeviceTests;
+
+[Register(nameof(AppDelegate))]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Platforms/iOS/Info.plist b/src/CommunityToolkit.Maui.DeviceTests/Platforms/iOS/Info.plist
new file mode 100644
index 0000000000..ba93e29079
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Platforms/iOS/Info.plist
@@ -0,0 +1,34 @@
+
+
+
+
+ LSRequiresIPhoneOS
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+ UIViewControllerBasedStatusBarAppearance
+
+
+
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Platforms/iOS/Program.cs b/src/CommunityToolkit.Maui.DeviceTests/Platforms/iOS/Program.cs
new file mode 100644
index 0000000000..12bbbfd3d4
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Platforms/iOS/Program.cs
@@ -0,0 +1,8 @@
+using UIKit;
+
+namespace CommunityToolkit.Maui.DeviceTests;
+
+public class Program
+{
+ static void Main(string[] args) => UIApplication.Main(args, null, typeof(AppDelegate));
+}
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Properties/launchSettings.json b/src/CommunityToolkit.Maui.DeviceTests/Properties/launchSettings.json
new file mode 100644
index 0000000000..edf8aadcc8
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Windows Machine": {
+ "commandName": "MsixPackage",
+ "nativeDebugging": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.DeviceTests/README.md b/src/CommunityToolkit.Maui.DeviceTests/README.md
new file mode 100644
index 0000000000..c082611739
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/README.md
@@ -0,0 +1,103 @@
+# CommunityToolkit.Maui.DeviceTests
+
+Device tests for the .NET MAUI Community Toolkit. These tests run on actual devices/emulators and verify platform-specific behavior that cannot be tested with unit tests alone.
+
+This project uses [DeviceRunners](https://github.com/mattleibow/DeviceRunners) by Matthew Leibowitz, the same test-runner infrastructure recommended by the .NET MAUI team. Tests are discovered and executed through `DeviceRunners.VisualRunners.Xunit`, with a built-in visual runner UI and `dotnet test` support via `DeviceRunners.Testing.Targets`.
+
+## Prerequisites
+
+- .NET SDK with MAUI workloads installed (`dotnet workload install maui`)
+- For Android: Android emulator or physical device connected
+- For iOS/MacCatalyst: macOS with Xcode installed
+- For Windows: Windows 10/11 with Windows App SDK
+
+## Running Tests
+
+### Visual Runner (IDE / Interactive)
+
+Launch the app like any other MAUI app — F5 in Visual Studio / VS Code, or:
+
+```bash
+dotnet build src/CommunityToolkit.Maui.DeviceTests/CommunityToolkit.Maui.DeviceTests.csproj -f net10.0-android -t:Run
+```
+
+The **DeviceRunners visual runner** UI displays test results with pass/fail counts, per-test details, and diagnostics.
+
+### `dotnet test` (CI / Headless, Recommended)
+
+```bash
+dotnet test src/CommunityToolkit.Maui.DeviceTests/CommunityToolkit.Maui.DeviceTests.csproj -f net10.0-android
+dotnet test src/CommunityToolkit.Maui.DeviceTests/CommunityToolkit.Maui.DeviceTests.csproj -f net10.0-ios
+dotnet test src/CommunityToolkit.Maui.DeviceTests/CommunityToolkit.Maui.DeviceTests.csproj -f net10.0-maccatalyst
+dotnet test src/CommunityToolkit.Maui.DeviceTests/CommunityToolkit.Maui.DeviceTests.csproj -f net10.0-windows10.0.19041.0
+```
+
+The `DeviceRunners.Testing.Targets` package hooks into `dotnet test` to build, deploy, run, and collect TRX results automatically. Filter tests with `--filter`:
+
+```bash
+dotnet test ... -f net10.0-android --filter "FullyQualifiedName~StatusBarBehavior"
+```
+
+## Architecture
+
+Powered by [DeviceRunners](https://github.com/mattleibow/DeviceRunners):
+
+- **`DeviceRunners.VisualRunners.Maui`** — MAUI visual runner UI (pages, view models, diagnostics)
+- **`DeviceRunners.VisualRunners.Xunit`** — xUnit v2 test discovery and execution adapter
+- **`DeviceRunners.Testing.Targets`** — MSBuild targets enabling `dotnet test` for device projects
+- **`DeviceRunners.Core`** / **`DeviceRunners.VisualRunners.Core`** — Core abstractions (test runners, result channels, formatters)
+
+Configured in `MauiProgram.cs`:
+
+```csharp
+builder.UseVisualTestRunner(conf => conf
+ .AddCliConfiguration()
+ .AddConsoleResultChannel()
+ .AddTestAssembly(typeof(MauiProgram).Assembly)
+ .AddXunit());
+```
+
+## Project Structure
+
+```text
+CommunityToolkit.Maui.DeviceTests/
+├── Platforms/
+│ ├── Android/ # Android-specific app entry points
+│ ├── iOS/ # iOS-specific app entry points
+│ ├── MacCatalyst/ # MacCatalyst-specific app entry points
+│ └── Windows/ # Windows-specific app entry points
+├── Resources/
+│ ├── AppIcon/ # App icon assets
+│ └── Splash/ # Splash screen assets
+├── Tests/ # Device test classes organized by area
+│ ├── Additional/ # Additional cross-cutting tests
+│ ├── Behaviors/ # Behavior tests
+│ ├── Camera/ # Camera package tests
+│ ├── Converters/ # Converter tests
+│ ├── Core/ # Core package tests (primitives, layouts, extensions, essentials)
+│ ├── Extensions/ # Internal extension tests
+│ ├── Maps/ # Maps package tests
+│ ├── MediaElement/ # MediaElement package tests
+│ └── Views/ # View tests
+├── Properties/
+│ └── launchSettings.json
+├── MauiProgram.cs # App builder / DI configuration
+├── GlobalUsings.cs # Assembly-level xunit configuration
+├── SmokeTests.cs # Basic app boot verification tests
+├── PlatformDetectionTests.cs # Platform detection tests
+└── HandlerTests.cs # Handler creation tests
+```
+
+## Adding New Tests
+
+1. Create a new test class in the `Tests/` folder (organized by area)
+2. Use `[Fact]` or `[Theory]` attributes from xunit v2
+3. For platform-specific tests, use `#if` directives or create `*.android.cs` / `*.ios.cs` / `*.windows.cs` / `*.macios.cs` files
+4. Tests run sequentially (parallelization is disabled) to avoid UI threading issues
+
+## Notes
+
+- Tests run inside a real MAUI application on the target platform. DeviceRunners handles test discovery, execution, and result collection — do **not** build custom `XunitFrontController` wrappers or `DeviceRunner` classes.
+- The app displays a visual test runner page while tests execute
+- This project does NOT produce a NuGet package — it is test-only
+- All NuGet packages (including transitive XHarness dependencies) come from nuget.org; no custom feeds are required
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Resources/AppIcon/appicon.svg b/src/CommunityToolkit.Maui.DeviceTests/Resources/AppIcon/appicon.svg
new file mode 100644
index 0000000000..b8d1fd8550
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Resources/AppIcon/appicon.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Resources/AppIcon/appiconfg.svg b/src/CommunityToolkit.Maui.DeviceTests/Resources/AppIcon/appiconfg.svg
new file mode 100644
index 0000000000..1c71d43f2d
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Resources/AppIcon/appiconfg.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Resources/Splash/splash.svg b/src/CommunityToolkit.Maui.DeviceTests/Resources/Splash/splash.svg
new file mode 100644
index 0000000000..b2baadfecc
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Resources/Splash/splash.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/src/CommunityToolkit.Maui.DeviceTests/SmokeTests.cs b/src/CommunityToolkit.Maui.DeviceTests/SmokeTests.cs
new file mode 100644
index 0000000000..ad41e000c8
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/SmokeTests.cs
@@ -0,0 +1,30 @@
+using Xunit;
+
+namespace CommunityToolkit.Maui.DeviceTests;
+
+public class SmokeTests
+{
+ [Fact]
+ public void ApplicationIsNotNull()
+ {
+ Assert.NotNull(Application.Current);
+ }
+
+ [Fact]
+ public void WindowPageIsNotNull()
+ {
+ Assert.NotNull(Application.Current?.Windows[0].Page);
+ }
+
+ [Fact]
+ public void DispatcherIsAvailable()
+ {
+ Assert.NotNull(Application.Current?.Dispatcher);
+ }
+
+ [Fact]
+ public void MauiAppServicesAreAvailable()
+ {
+ Assert.NotNull(Application.Current?.Handler?.MauiContext?.Services);
+ }
+}
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/AdditionalMauiTests.cs b/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/AdditionalMauiTests.cs
new file mode 100644
index 0000000000..9b8ba76bfc
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/AdditionalMauiTests.cs
@@ -0,0 +1,385 @@
+using CommunityToolkit.Maui.Animations;
+using CommunityToolkit.Maui.Core;
+using CommunityToolkit.Maui.ImageSources;
+using CommunityToolkit.Maui.Layouts;
+using Xunit;
+
+namespace CommunityToolkit.Maui.DeviceTests.Tests.Additional;
+
+public class GravatarImageSourceTests
+{
+ [Fact]
+ public void GravatarImageSource_DefaultProperties()
+ {
+ var source = new GravatarImageSource();
+
+ Assert.Null(source.Email);
+ Assert.Equal(DefaultImage.MysteryPerson, source.Image);
+ Assert.True(source.CachingEnabled);
+ Assert.Equal(TimeSpan.FromDays(1), source.CacheValidity);
+ Assert.True(source.IsEmpty);
+ }
+
+ [Fact]
+ public void GravatarImageSource_SetEmail_IsNotEmpty()
+ {
+ var source = new GravatarImageSource
+ {
+ Email = "test@example.com"
+ };
+
+ Assert.False(source.IsEmpty);
+ }
+
+ [Fact]
+ public void GravatarImageSource_SetEmail_UpdatesUri()
+ {
+ var source = new GravatarImageSource
+ {
+ Email = "test@example.com"
+ };
+
+ Assert.NotNull(source.Uri);
+ Assert.Contains("gravatar.com/avatar/", source.Uri.ToString());
+ }
+
+ [Fact]
+ public void GravatarImageSource_CanSetImage()
+ {
+ var source = new GravatarImageSource
+ {
+ Image = DefaultImage.Robohash
+ };
+
+ Assert.Equal(DefaultImage.Robohash, source.Image);
+ }
+
+ [Fact]
+ public void GravatarImageSource_CanDisableCaching()
+ {
+ var source = new GravatarImageSource
+ {
+ CachingEnabled = false
+ };
+
+ Assert.False(source.CachingEnabled);
+ }
+
+ [Fact]
+ public void GravatarImageSource_CanSetCacheValidity()
+ {
+ var source = new GravatarImageSource
+ {
+ CacheValidity = TimeSpan.FromHours(2)
+ };
+
+ Assert.Equal(TimeSpan.FromHours(2), source.CacheValidity);
+ }
+
+ [Fact]
+ public void DefaultImage_HasExpectedValues()
+ {
+ Assert.Equal(0, (int)DefaultImage.MysteryPerson);
+ Assert.Equal(1, (int)DefaultImage.FileNotFound);
+ Assert.Equal(2, (int)DefaultImage.Identicon);
+ Assert.Equal(3, (int)DefaultImage.MonsterId);
+ Assert.Equal(4, (int)DefaultImage.Wavatar);
+ Assert.Equal(5, (int)DefaultImage.Retro);
+ Assert.Equal(6, (int)DefaultImage.Robohash);
+ Assert.Equal(7, (int)DefaultImage.Blank);
+ }
+}
+
+public class FadeAnimationTests
+{
+ [Fact]
+ public void FadeAnimation_DefaultProperties()
+ {
+ var animation = new FadeAnimation();
+
+ Assert.True(animation.Length > 0);
+ Assert.NotNull(animation.Easing);
+ }
+
+ [Fact]
+ public void FadeAnimation_CanSetOpacity()
+ {
+ var animation = new FadeAnimation
+ {
+ Opacity = 0.5
+ };
+
+ Assert.Equal(0.5, animation.Opacity);
+ }
+
+ [Fact]
+ public void FadeAnimation_CanSetLength()
+ {
+ var animation = new FadeAnimation
+ {
+ Length = 500
+ };
+
+ Assert.Equal(500u, animation.Length);
+ }
+
+ [Fact]
+ public void FadeAnimation_CanSetEasing()
+ {
+ var animation = new FadeAnimation
+ {
+ Easing = Easing.CubicOut
+ };
+
+ Assert.Equal(Easing.CubicOut, animation.Easing);
+ }
+}
+
+public class DockLayoutTests
+{
+ [Fact]
+ public void DockLayout_DefaultProperties()
+ {
+ var dockLayout = new DockLayout();
+
+ Assert.True(dockLayout.ShouldExpandLastChild);
+ Assert.Equal(0, dockLayout.HorizontalSpacing);
+ Assert.Equal(0, dockLayout.VerticalSpacing);
+ }
+
+ [Fact]
+ public void DockLayout_CanSetSpacing()
+ {
+ var dockLayout = new DockLayout
+ {
+ HorizontalSpacing = 10,
+ VerticalSpacing = 5
+ };
+
+ Assert.Equal(10, dockLayout.HorizontalSpacing);
+ Assert.Equal(5, dockLayout.VerticalSpacing);
+ }
+
+ [Fact]
+ public void DockLayout_CanSetShouldExpandLastChild()
+ {
+ var dockLayout = new DockLayout
+ {
+ ShouldExpandLastChild = false
+ };
+
+ Assert.False(dockLayout.ShouldExpandLastChild);
+ }
+
+ [Fact]
+ public void DockLayout_GetSetDockPosition()
+ {
+ var view = new Label();
+ var dockLayout = new DockLayout();
+
+ DockLayout.SetDockPosition(view, DockPosition.Right);
+
+ Assert.Equal(DockPosition.Right, DockLayout.GetDockPosition(view));
+ }
+
+ [Fact]
+ public void DockLayout_DefaultDockPosition_IsNone()
+ {
+ var view = new Label();
+
+ Assert.Equal(DockPosition.None, DockLayout.GetDockPosition(view));
+ }
+
+ [Fact]
+ public void DockLayout_Add_WithPosition()
+ {
+ var dockLayout = new DockLayout();
+ var view = new Label();
+
+ dockLayout.Add(view, DockPosition.Left);
+
+ Assert.Contains(view, dockLayout.Children);
+ Assert.Equal(DockPosition.Left, DockLayout.GetDockPosition(view));
+ }
+}
+
+public class UniformItemsLayoutTests
+{
+ [Fact]
+ public void UniformItemsLayout_DefaultProperties()
+ {
+ var layout = new UniformItemsLayout();
+
+ Assert.Equal(int.MaxValue, layout.MaxRows);
+ Assert.Equal(int.MaxValue, layout.MaxColumns);
+ }
+
+ [Fact]
+ public void UniformItemsLayout_CanSetMaxRows()
+ {
+ var layout = new UniformItemsLayout
+ {
+ MaxRows = 3
+ };
+
+ Assert.Equal(3, layout.MaxRows);
+ }
+
+ [Fact]
+ public void UniformItemsLayout_CanSetMaxColumns()
+ {
+ var layout = new UniformItemsLayout
+ {
+ MaxColumns = 4
+ };
+
+ Assert.Equal(4, layout.MaxColumns);
+ }
+
+ [Fact]
+ public void UniformItemsLayout_MaxRowsLessThanOne_Throws()
+ {
+ var layout = new UniformItemsLayout();
+ var thrown = false;
+
+ try
+ {
+ layout.MaxRows = 0;
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ thrown = true;
+ }
+
+ Assert.True(thrown);
+ }
+
+ [Fact]
+ public void UniformItemsLayout_MaxColumnsLessThanOne_Throws()
+ {
+ var layout = new UniformItemsLayout();
+ var thrown = false;
+
+ try
+ {
+ layout.MaxColumns = 0;
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ thrown = true;
+ }
+
+ Assert.True(thrown);
+ }
+}
+
+public class StateContainerTests
+{
+ [Fact]
+ public void StateContainer_GetSetCurrentState()
+ {
+ var layout = new Grid();
+
+ // StateContainer requires StateViews to be configured before setting CurrentState,
+ // otherwise it throws "Unable to determine StateView for State".
+ var loadingView = new Label { Text = "Loading" };
+ StateView.SetStateKey(loadingView, "Loading");
+ StateContainer.SetStateViews(layout, [loadingView]);
+
+ StateContainer.SetCurrentState(layout, "Loading");
+
+ Assert.Equal("Loading", StateContainer.GetCurrentState(layout));
+ }
+
+ [Fact]
+ public void StateContainer_DefaultCurrentState_IsNull()
+ {
+ var layout = new Grid();
+
+ Assert.Null(StateContainer.GetCurrentState(layout));
+ }
+
+ [Fact]
+ public void StateContainer_GetCanStateChange_DefaultTrue()
+ {
+ var layout = new Grid();
+
+ Assert.True(StateContainer.GetCanStateChange(layout));
+ }
+
+ [Fact]
+ public void StateContainer_CanStateChangeProperty_Exists()
+ {
+ Assert.NotNull(StateContainer.CanStateChangeProperty);
+ }
+
+ [Fact]
+ public void StateContainer_StateViews_DefaultEmpty()
+ {
+ var layout = new Grid();
+ var stateViews = StateContainer.GetStateViews(layout);
+
+ Assert.NotNull(stateViews);
+ Assert.Empty(stateViews);
+ }
+}
+
+public class StateViewTests
+{
+ [Fact]
+ public void StateView_GetSetStateKey()
+ {
+ var view = new Label();
+
+ StateView.SetStateKey(view, "Error");
+
+ Assert.Equal("Error", StateView.GetStateKey(view));
+ }
+
+ [Fact]
+ public void StateView_DefaultStateKey()
+ {
+ var view = new Label();
+ var stateKey = StateView.GetStateKey(view);
+
+ Assert.NotNull(stateKey);
+ }
+}
+
+public class ExpandedChangedEventArgsTests
+{
+ [Fact]
+ public void ExpandedChangedEventArgs_SetsIsExpanded()
+ {
+ var args = new ExpandedChangedEventArgs(true);
+
+ Assert.True(args.IsExpanded);
+ }
+
+ [Fact]
+ public void ExpandedChangedEventArgs_FalseValue()
+ {
+ var args = new ExpandedChangedEventArgs(false);
+
+ Assert.False(args.IsExpanded);
+ }
+}
+
+public class RatingChangedEventArgsTests
+{
+ [Fact]
+ public void RatingChangedEventArgs_SetsRating()
+ {
+ var args = new RatingChangedEventArgs(3.5);
+
+ Assert.Equal(3.5, args.Rating);
+ }
+
+ [Fact]
+ public void RatingChangedEventArgs_ZeroRating()
+ {
+ var args = new RatingChangedEventArgs(0);
+
+ Assert.Equal(0, args.Rating);
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/ColorAndThemeTests.cs b/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/ColorAndThemeTests.cs
new file mode 100644
index 0000000000..218ef25060
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/ColorAndThemeTests.cs
@@ -0,0 +1,579 @@
+using CommunityToolkit.Maui.Core;
+using CommunityToolkit.Maui.Core.Extensions;
+using CommunityToolkit.Maui.Core.Primitives;
+using CommunityToolkit.Maui.Core.Views;
+using Xunit;
+
+namespace CommunityToolkit.Maui.DeviceTests.Tests.Additional;
+
+public class ColorConversionWithMethodsTests
+{
+ [Fact]
+ public void WithRed_Double_ReturnsModifiedColor()
+ {
+ var color = new Color(0f, 0.5f, 0.5f);
+
+ var result = color.WithRed(1.0);
+
+ Assert.Equal(1.0, result.Red, 0.01);
+ Assert.Equal(0.5, result.Green, 0.01);
+ Assert.Equal(0.5, result.Blue, 0.01);
+ }
+
+ [Fact]
+ public void WithRed_Double_OutOfRange_Throws()
+ {
+ var color = Colors.Red;
+ var thrown1 = false;
+ var thrown2 = false;
+
+ try
+ {
+ color.WithRed(1.5);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ thrown1 = true;
+ }
+
+ try
+ {
+ color.WithRed(-0.1);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ thrown2 = true;
+ }
+
+ Assert.True(thrown1);
+ Assert.True(thrown2);
+ }
+
+ [Fact]
+ public void WithGreen_Double_ReturnsModifiedColor()
+ {
+ var color = new Color(0.5f, 0f, 0.5f);
+
+ var result = color.WithGreen(1.0);
+
+ Assert.Equal(0.5, result.Red, 0.01);
+ Assert.Equal(1.0, result.Green, 0.01);
+ Assert.Equal(0.5, result.Blue, 0.01);
+ }
+
+ [Fact]
+ public void WithGreen_Double_OutOfRange_Throws()
+ {
+ var color = Colors.Green;
+ var thrown1 = false;
+ var thrown2 = false;
+
+ try
+ {
+ color.WithGreen(2.0);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ thrown1 = true;
+ }
+
+ try
+ {
+ color.WithGreen(-1.0);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ thrown2 = true;
+ }
+
+ Assert.True(thrown1);
+ Assert.True(thrown2);
+ }
+
+ [Fact]
+ public void WithBlue_Double_ReturnsModifiedColor()
+ {
+ var color = new Color(0.5f, 0.5f, 0f);
+
+ var result = color.WithBlue(1.0);
+
+ Assert.Equal(0.5, result.Red, 0.01);
+ Assert.Equal(0.5, result.Green, 0.01);
+ Assert.Equal(1.0, result.Blue, 0.01);
+ }
+
+ [Fact]
+ public void WithBlue_Double_OutOfRange_Throws()
+ {
+ var color = Colors.Blue;
+ var thrown1 = false;
+ var thrown2 = false;
+
+ try
+ {
+ color.WithBlue(1.01);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ thrown1 = true;
+ }
+
+ try
+ {
+ color.WithBlue(-0.01);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ thrown2 = true;
+ }
+
+ Assert.True(thrown1);
+ Assert.True(thrown2);
+ }
+
+ [Fact]
+ public void WithRed_Byte_ReturnsModifiedColor()
+ {
+ var color = new Color(0f, 0f, 0f);
+
+ var result = color.WithRed((byte)255);
+
+ Assert.Equal(1.0, result.Red, 0.01);
+ }
+
+ [Fact]
+ public void WithGreen_Byte_ReturnsModifiedColor()
+ {
+ var color = new Color(0f, 0f, 0f);
+
+ var result = color.WithGreen((byte)128);
+
+ Assert.True(result.Green > 0.49 && result.Green < 0.51);
+ }
+
+ [Fact]
+ public void WithBlue_Byte_ReturnsModifiedColor()
+ {
+ var color = new Color(0f, 0f, 0f);
+
+ var result = color.WithBlue((byte)255);
+
+ Assert.Equal(1.0, result.Blue, 0.01);
+ }
+
+ [Fact]
+ public void WithCyan_ReturnsModifiedColor()
+ {
+ var color = Colors.Red;
+
+ var result = color.WithCyan(0.5);
+
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ public void WithMagenta_ReturnsModifiedColor()
+ {
+ var color = Colors.Green;
+
+ var result = color.WithMagenta(0.5);
+
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ public void WithYellow_ReturnsModifiedColor()
+ {
+ var color = Colors.Blue;
+
+ var result = color.WithYellow(0.5);
+
+ Assert.NotNull(result);
+ }
+
+ [Fact]
+ public void WithBlackKey_ReturnsModifiedColor()
+ {
+ var color = Colors.White;
+
+ var result = color.WithBlackKey(0.5);
+
+ Assert.NotNull(result);
+ }
+}
+
+public class ColorConversionGetMethodsTests
+{
+ [Fact]
+ public void GetByteRed_ReturnsCorrectValue()
+ {
+ var color = new Color(1f, 0f, 0f);
+
+ Assert.Equal(255, color.GetByteRed());
+ }
+
+ [Fact]
+ public void GetByteGreen_ReturnsCorrectValue()
+ {
+ var color = new Color(0f, 1f, 0f);
+
+ Assert.Equal(255, color.GetByteGreen());
+ }
+
+ [Fact]
+ public void GetByteBlue_ReturnsCorrectValue()
+ {
+ var color = new Color(0f, 0f, 1f);
+
+ Assert.Equal(255, color.GetByteBlue());
+ }
+
+ [Fact]
+ public void GetByteAlpha_ReturnsCorrectValue()
+ {
+ var color = new Color(0f, 0f, 0f, 1f);
+
+ Assert.Equal(255, color.GetByteAlpha());
+ }
+
+ [Fact]
+ public void GetByteAlpha_HalfTransparent()
+ {
+ var color = new Color(0f, 0f, 0f, 0.5f);
+
+ Assert.Equal(128, color.GetByteAlpha());
+ }
+
+ [Fact]
+ public void GetDegreeHue_ScalesHueTo360()
+ {
+ var color = Colors.Red;
+
+ // GetDegreeHue is defined as GetHue() * 360; verify the contract rather than a specific color value
+ Assert.Equal(color.GetHue() * 360, color.GetDegreeHue(), 3);
+ }
+
+ [Fact]
+ public void GetPercentBlackKey_White_IsZero()
+ {
+ var color = Colors.White;
+
+ Assert.Equal(0f, color.GetPercentBlackKey(), 0.01f);
+ }
+
+ [Fact]
+ public void GetPercentBlackKey_Black_IsOne()
+ {
+ var color = Colors.Black;
+
+ Assert.Equal(1f, color.GetPercentBlackKey(), 0.01f);
+ }
+
+ [Fact]
+ public void GetPercentCyan_Red_IsZero()
+ {
+ var color = Colors.Red;
+
+ Assert.Equal(0f, color.GetPercentCyan(), 0.01f);
+ }
+
+ [Fact]
+ public void GetPercentMagenta_Green_IsZero()
+ {
+ var color = Colors.Green;
+
+ Assert.Equal(0f, color.GetPercentMagenta(), 0.01f);
+ }
+
+ [Fact]
+ public void GetPercentYellow_Blue_IsZero()
+ {
+ var color = Colors.Blue;
+
+ Assert.Equal(0f, color.GetPercentYellow(), 0.01f);
+ }
+}
+
+public class ColorConversionToMethodsTests
+{
+ [Fact]
+ public void ToInverseColor_Red_ReturnsCyan()
+ {
+ var color = Colors.Red;
+
+ var result = color.ToInverseColor();
+
+ Assert.Equal(0f, result.Red, 0.01f);
+ Assert.Equal(1f, result.Green, 0.01f);
+ Assert.Equal(1f, result.Blue, 0.01f);
+ }
+
+ [Fact]
+ public void ToInverseColor_Black_ReturnsWhite()
+ {
+ var color = Colors.Black;
+
+ var result = color.ToInverseColor();
+
+ Assert.Equal(1f, result.Red, 0.01f);
+ Assert.Equal(1f, result.Green, 0.01f);
+ Assert.Equal(1f, result.Blue, 0.01f);
+ }
+
+ [Fact]
+ public void ToBlackOrWhite_LightColor_ReturnsWhite()
+ {
+ var color = Colors.White;
+
+ var result = color.ToBlackOrWhite();
+
+ Assert.Equal(Colors.White, result);
+ }
+
+ [Fact]
+ public void ToBlackOrWhite_DarkColor_ReturnsBlack()
+ {
+ var color = Colors.Black;
+
+ var result = color.ToBlackOrWhite();
+
+ Assert.Equal(Colors.Black, result);
+ }
+
+ [Fact]
+ public void ToGrayScale_ReturnsGray()
+ {
+ var color = Colors.Red;
+
+ var result = color.ToGrayScale();
+
+ // Grayscale should have equal R, G, B components
+ Assert.Equal(result.Red, result.Green, 0.01f);
+ Assert.Equal(result.Green, result.Blue, 0.01f);
+ }
+}
+
+public class AppThemeObjectTests
+{
+ [Fact]
+ public void AppThemeColor_DefaultProperties_AreNull()
+ {
+ var appThemeColor = new AppThemeColor();
+
+ Assert.Null(appThemeColor.Light);
+ Assert.Null(appThemeColor.Dark);
+ Assert.Null(appThemeColor.Default);
+ }
+
+ [Fact]
+ public void AppThemeColor_CanSetLightDarkDefault()
+ {
+ var appThemeColor = new AppThemeColor
+ {
+ Light = Colors.White,
+ Dark = Colors.Black,
+ Default = Colors.Gray
+ };
+
+ Assert.Equal(Colors.White, appThemeColor.Light);
+ Assert.Equal(Colors.Black, appThemeColor.Dark);
+ Assert.Equal(Colors.Gray, appThemeColor.Default);
+ }
+
+ [Fact]
+ public void AppThemeObject_DefaultProperties_AreNull()
+ {
+ var appThemeObject = new AppThemeObject();
+
+ Assert.Null(appThemeObject.Light);
+ Assert.Null(appThemeObject.Dark);
+ Assert.Null(appThemeObject.Default);
+ }
+
+ [Fact]
+ public void AppThemeObject_CanSetLightDarkDefault()
+ {
+ var appThemeObject = new AppThemeObject
+ {
+ Light = "LightValue",
+ Dark = "DarkValue",
+ Default = "DefaultValue"
+ };
+
+ Assert.Equal("LightValue", appThemeObject.Light);
+ Assert.Equal("DarkValue", appThemeObject.Dark);
+ Assert.Equal("DefaultValue", appThemeObject.Default);
+ }
+
+ [Fact]
+ public void AppThemeColor_GetBinding_ReturnsBinding()
+ {
+ var appThemeColor = new AppThemeColor
+ {
+ Light = Colors.White,
+ Dark = Colors.Black
+ };
+
+ var binding = appThemeColor.GetBinding();
+
+ Assert.NotNull(binding);
+ }
+
+ [Fact]
+ public void AppThemeObject_GetBinding_ReturnsBinding()
+ {
+ var appThemeObject = new AppThemeObject
+ {
+ Light = 42,
+ Dark = 24
+ };
+
+ var binding = appThemeObject.GetBinding();
+
+ Assert.NotNull(binding);
+ }
+}
+
+public class SnackbarOptionsTests
+{
+ [Fact]
+ public void SnackbarOptions_DefaultProperties()
+ {
+ var options = new SnackbarOptions();
+
+ Assert.Equal(0.0, options.CharacterSpacing);
+ Assert.Equal(Colors.Black, options.TextColor);
+ Assert.Equal(Colors.Black, options.ActionButtonTextColor);
+ Assert.Equal(Colors.LightGray, options.BackgroundColor);
+ Assert.Equal(new CornerRadius(4, 4, 4, 4), options.CornerRadius);
+ }
+
+ [Fact]
+ public void SnackbarOptions_CanSetProperties()
+ {
+ var options = new SnackbarOptions
+ {
+ CharacterSpacing = 2.0,
+ TextColor = Colors.Red,
+ ActionButtonTextColor = Colors.Blue,
+ BackgroundColor = Colors.Yellow,
+ CornerRadius = new CornerRadius(8)
+ };
+
+ Assert.Equal(2.0, options.CharacterSpacing);
+ Assert.Equal(Colors.Red, options.TextColor);
+ Assert.Equal(Colors.Blue, options.ActionButtonTextColor);
+ Assert.Equal(Colors.Yellow, options.BackgroundColor);
+ Assert.Equal(new CornerRadius(8), options.CornerRadius);
+ }
+
+ [Fact]
+ public void SnackbarOptions_CanSetFont()
+ {
+ var font = Microsoft.Maui.Font.SystemFontOfSize(20);
+ var options = new SnackbarOptions
+ {
+ Font = font,
+ ActionButtonFont = font
+ };
+
+ Assert.Equal(font, options.Font);
+ Assert.Equal(font, options.ActionButtonFont);
+ }
+}
+
+public class FolderRecordTests
+{
+ [Fact]
+ public void Folder_StoresPathAndName()
+ {
+ var folder = new Folder("/tmp/test", "test");
+
+ Assert.Equal("/tmp/test", folder.Path);
+ Assert.Equal("test", folder.Name);
+ }
+
+ [Fact]
+ public void Folder_Equality_SameValues()
+ {
+ var folder1 = new Folder("/tmp/test", "test");
+ var folder2 = new Folder("/tmp/test", "test");
+
+ Assert.Equal(folder1, folder2);
+ }
+
+ [Fact]
+ public void Folder_Inequality_DifferentValues()
+ {
+ var folder1 = new Folder("/tmp/test1", "test1");
+ var folder2 = new Folder("/tmp/test2", "test2");
+
+ Assert.NotEqual(folder1, folder2);
+ }
+
+ [Fact]
+ public void Folder_WithExpression_CreatesCopy()
+ {
+ var folder = new Folder("/tmp/test", "test");
+ var modified = folder with { Name = "modified" };
+
+ Assert.Equal("/tmp/test", modified.Path);
+ Assert.Equal("modified", modified.Name);
+ Assert.Equal("test", folder.Name);
+ }
+
+ [Fact]
+ public void Folder_ToString_ContainsValues()
+ {
+ var folder = new Folder("/tmp/test", "test");
+ var str = folder.ToString();
+
+ Assert.Contains("/tmp/test", str);
+ Assert.Contains("test", str);
+ }
+}
+
+public class CoreEventArgsAdditionalTests
+{
+ [Fact]
+ public void TouchGestureCompletedEventArgs_CarriesParameter()
+ {
+ var args = new TouchGestureCompletedEventArgs("param");
+
+ Assert.Equal("param", args.TouchCommandParameter);
+ }
+
+ [Fact]
+ public void TouchGestureCompletedEventArgs_NullParameter()
+ {
+ var args = new TouchGestureCompletedEventArgs(null);
+
+ Assert.Null(args.TouchCommandParameter);
+ }
+
+ [Fact]
+ public void LongPressCompletedEventArgs_NullParameter()
+ {
+ var args = new LongPressCompletedEventArgs(null);
+
+ Assert.Null(args.LongPressCommandParameter);
+ }
+
+ [Fact]
+ public void DrawingLineCompletedEventArgs_CarriesLine()
+ {
+ var line = new DrawingLine();
+ var args = new DrawingLineCompletedEventArgs(line);
+
+ Assert.Same(line, args.LastDrawingLine);
+ }
+
+ [Fact]
+ public void DrawingLineStartedEventArgs_CarriesPoint()
+ {
+ var point = new PointF(5f, 10f);
+ var args = new DrawingLineStartedEventArgs(point);
+
+ Assert.Equal(point, args.Point);
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/ConvertersAndBehaviorsTests.cs b/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/ConvertersAndBehaviorsTests.cs
new file mode 100644
index 0000000000..d9d5000bb7
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/ConvertersAndBehaviorsTests.cs
@@ -0,0 +1,520 @@
+using System.Globalization;
+using CommunityToolkit.Maui.Behaviors;
+using CommunityToolkit.Maui.Converters;
+using Xunit;
+
+namespace CommunityToolkit.Maui.DeviceTests.Tests.Additional;
+
+public class ColorToStringConverterTests
+{
+ [Fact]
+ public void ColorToRgbStringConverter_ConvertFrom_Red()
+ {
+ var converter = new ColorToRgbStringConverter();
+
+ var result = converter.ConvertFrom(Colors.Red);
+
+ Assert.Equal("RGB(255,0,0)", result);
+ }
+
+ [Fact]
+ public void ColorToRgbStringConverter_ConvertBackTo_ValidColor()
+ {
+ var converter = new ColorToRgbStringConverter();
+
+ var result = converter.ConvertBackTo("#FF0000", null);
+
+ Assert.Equal(Colors.Red, result);
+ }
+
+ [Fact]
+ public void ColorToRgbStringConverter_ConvertBackTo_InvalidColor_ReturnsDefault()
+ {
+ var converter = new ColorToRgbStringConverter();
+
+ var result = converter.ConvertBackTo("not-a-color", null);
+
+ Assert.Equal(Colors.Transparent, result);
+ }
+
+ [Fact]
+ public void ColorToRgbaStringConverter_ConvertFrom_Red()
+ {
+ var converter = new ColorToRgbaStringConverter();
+
+ var result = converter.ConvertFrom(Colors.Red);
+
+ Assert.StartsWith("RGBA(", result);
+ }
+
+ [Fact]
+ public void ColorToHexRgbStringConverter_ConvertFrom_Red()
+ {
+ var converter = new ColorToHexRgbStringConverter();
+
+ var result = converter.ConvertFrom(Colors.Red);
+
+ Assert.Equal("#FF0000", result);
+ }
+
+ [Fact]
+ public void ColorToHexRgbStringConverter_ConvertBackTo()
+ {
+ var converter = new ColorToHexRgbStringConverter();
+
+ var result = converter.ConvertBackTo("#FF0000");
+
+ Assert.Equal(1f, result.Red, 0.01f);
+ Assert.Equal(0f, result.Green, 0.01f);
+ Assert.Equal(0f, result.Blue, 0.01f);
+ }
+
+ [Fact]
+ public void ColorToHexRgbaStringConverter_ConvertFrom_Red()
+ {
+ var converter = new ColorToHexRgbaStringConverter();
+
+ var result = converter.ConvertFrom(Colors.Red);
+
+ Assert.Equal("#FF0000FF", result);
+ }
+
+ [Fact]
+ public void ColorToHexArgbStringConverter_ConvertFrom_Red()
+ {
+ var converter = new ColorToHexArgbStringConverter();
+
+ var result = converter.ConvertFrom(Colors.Red);
+
+ Assert.Equal("#FFFF0000", result);
+ }
+
+ [Fact]
+ public void ColorToCmykStringConverter_ConvertFrom_Red()
+ {
+ var converter = new ColorToCmykStringConverter();
+
+ var result = converter.ConvertFrom(Colors.Red);
+
+ Assert.StartsWith("CMYK(", result);
+ }
+
+ [Fact]
+ public void ColorToCmykaStringConverter_ConvertFrom_Red()
+ {
+ var converter = new ColorToCmykaStringConverter();
+
+ var result = converter.ConvertFrom(Colors.Red);
+
+ Assert.StartsWith("CMYKA(", result);
+ }
+
+ [Fact]
+ public void ColorToHslStringConverter_ConvertFrom_Red()
+ {
+ var converter = new ColorToHslStringConverter();
+
+ var result = converter.ConvertFrom(Colors.Red);
+
+ Assert.StartsWith("HSL(", result);
+ }
+
+ [Fact]
+ public void ColorToHslaStringConverter_ConvertFrom_Red()
+ {
+ var converter = new ColorToHslaStringConverter();
+
+ var result = converter.ConvertFrom(Colors.Red);
+
+ Assert.StartsWith("HSLA(", result);
+ }
+
+ [Fact]
+ public void ColorToStringConverters_DefaultConvertReturnValue_IsEmpty()
+ {
+ Assert.Equal(string.Empty, new ColorToRgbStringConverter().DefaultConvertReturnValue);
+ Assert.Equal(string.Empty, new ColorToRgbaStringConverter().DefaultConvertReturnValue);
+ Assert.Equal(string.Empty, new ColorToHexRgbStringConverter().DefaultConvertReturnValue);
+ Assert.Equal(string.Empty, new ColorToHexRgbaStringConverter().DefaultConvertReturnValue);
+ Assert.Equal(string.Empty, new ColorToHexArgbStringConverter().DefaultConvertReturnValue);
+ Assert.Equal(string.Empty, new ColorToCmykStringConverter().DefaultConvertReturnValue);
+ Assert.Equal(string.Empty, new ColorToCmykaStringConverter().DefaultConvertReturnValue);
+ Assert.Equal(string.Empty, new ColorToHslStringConverter().DefaultConvertReturnValue);
+ Assert.Equal(string.Empty, new ColorToHslaStringConverter().DefaultConvertReturnValue);
+ }
+}
+
+public class MultiConverterTests
+{
+ [Fact]
+ public void MultiConverter_Convert_ChainsConverters()
+ {
+ var multiConverter = new MultiConverter
+ {
+ new InvertedBoolConverter()
+ };
+
+ var result = multiConverter.Convert(true, typeof(bool), null, CultureInfo.InvariantCulture);
+
+ Assert.Equal(false, result);
+ }
+
+ [Fact]
+ public void MultiConverter_Convert_MultipleConverters()
+ {
+ var multiConverter = new MultiConverter
+ {
+ new InvertedBoolConverter(),
+ new InvertedBoolConverter()
+ };
+
+ // true -> false -> true
+ var result = multiConverter.Convert(true, typeof(bool), null, CultureInfo.InvariantCulture);
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void MultiConverter_ConvertBack_ThrowsNotSupported()
+ {
+ var multiConverter = new MultiConverter();
+ var thrown = false;
+
+ try
+ {
+ multiConverter.ConvertBack(true, typeof(bool), null, CultureInfo.InvariantCulture);
+ }
+ catch (NotSupportedException)
+ {
+ thrown = true;
+ }
+
+ Assert.True(thrown);
+ }
+
+ [Fact]
+ public void MultiConverter_IsList()
+ {
+ var multiConverter = new MultiConverter();
+
+ Assert.IsAssignableFrom>(multiConverter);
+ }
+}
+
+public class MultiConverterParameterTests
+{
+ [Fact]
+ public void MultiConverterParameter_DefaultProperties()
+ {
+ var param = new MultiConverterParameter();
+
+ Assert.Null(param.ConverterType);
+ Assert.Null(param.Value);
+ }
+
+ [Fact]
+ public void MultiConverterParameter_CanSetProperties()
+ {
+ var param = new MultiConverterParameter
+ {
+ ConverterType = typeof(InvertedBoolConverter),
+ Value = 42
+ };
+
+ Assert.Equal(typeof(InvertedBoolConverter), param.ConverterType);
+ Assert.Equal(42, param.Value);
+ }
+}
+
+public class ByteArrayToImageSourceConverterTests
+{
+ [Fact]
+ public void ByteArrayToImageSourceConverter_DefaultConvertReturnValue_IsNull()
+ {
+ var converter = new ByteArrayToImageSourceConverter();
+
+ Assert.Null(converter.DefaultConvertReturnValue);
+ }
+
+ [Fact]
+ public void ByteArrayToImageSourceConverter_DefaultConvertBackReturnValue_IsNull()
+ {
+ var converter = new ByteArrayToImageSourceConverter();
+
+ // The converter's DefaultConvertBackReturnValue is initialized to null
+ Assert.Null(converter.DefaultConvertBackReturnValue);
+ }
+}
+
+public class MaskedBehaviorTests
+{
+ [Fact]
+ public void MaskedBehavior_DefaultMask_IsNull()
+ {
+ var behavior = new MaskedBehavior();
+
+ Assert.Null(behavior.Mask);
+ }
+
+ [Fact]
+ public void MaskedBehavior_DefaultUnmaskedCharacter_IsX()
+ {
+ var behavior = new MaskedBehavior();
+
+ Assert.Equal('X', behavior.UnmaskedCharacter);
+ }
+
+ [Fact]
+ public void MaskedBehavior_CanSetMask()
+ {
+ var behavior = new MaskedBehavior
+ {
+ Mask = "XX-XX-XX"
+ };
+
+ Assert.Equal("XX-XX-XX", behavior.Mask);
+ }
+
+ [Fact]
+ public void MaskedBehavior_CanSetUnmaskedCharacter()
+ {
+ var behavior = new MaskedBehavior
+ {
+ UnmaskedCharacter = '0'
+ };
+
+ Assert.Equal('0', behavior.UnmaskedCharacter);
+ }
+}
+
+public class MaxLengthReachedBehaviorTests
+{
+ [Fact]
+ public void MaxLengthReachedBehavior_DefaultCommand_IsNull()
+ {
+ var behavior = new MaxLengthReachedBehavior();
+
+ Assert.Null(behavior.Command);
+ }
+
+ [Fact]
+ public void MaxLengthReachedBehavior_DefaultShouldDismissKeyboard_IsFalse()
+ {
+ var behavior = new MaxLengthReachedBehavior();
+
+ Assert.False(behavior.ShouldDismissKeyboardAutomatically);
+ }
+
+ [Fact]
+ public void MaxLengthReachedBehavior_CanSetShouldDismissKeyboard()
+ {
+ var behavior = new MaxLengthReachedBehavior
+ {
+ ShouldDismissKeyboardAutomatically = true
+ };
+
+ Assert.True(behavior.ShouldDismissKeyboardAutomatically);
+ }
+}
+
+public class MaxLengthReachedEventArgsTests
+{
+ [Fact]
+ public void MaxLengthReachedEventArgs_CarriesText()
+ {
+ var args = new MaxLengthReachedEventArgs("hello");
+
+ Assert.Equal("hello", args.Text);
+ }
+
+ [Fact]
+ public void MaxLengthReachedEventArgs_EmptyText()
+ {
+ var args = new MaxLengthReachedEventArgs(string.Empty);
+
+ Assert.Equal(string.Empty, args.Text);
+ }
+}
+
+public class UserStoppedTypingBehaviorTests
+{
+ [Fact]
+ public void UserStoppedTypingBehavior_DefaultCommand_IsNull()
+ {
+ var behavior = new UserStoppedTypingBehavior();
+
+ Assert.Null(behavior.Command);
+ }
+
+ [Fact]
+ public void UserStoppedTypingBehavior_DefaultStoppedTypingTimeThreshold()
+ {
+ var behavior = new UserStoppedTypingBehavior();
+
+ Assert.Equal(1000, behavior.StoppedTypingTimeThreshold);
+ }
+
+ [Fact]
+ public void UserStoppedTypingBehavior_DefaultMinimumLengthThreshold()
+ {
+ var behavior = new UserStoppedTypingBehavior();
+
+ Assert.Equal(0, behavior.MinimumLengthThreshold);
+ }
+
+ [Fact]
+ public void UserStoppedTypingBehavior_DefaultShouldDismissKeyboard_IsFalse()
+ {
+ var behavior = new UserStoppedTypingBehavior();
+
+ Assert.False(behavior.ShouldDismissKeyboardAutomatically);
+ }
+
+ [Fact]
+ public void UserStoppedTypingBehavior_CanSetProperties()
+ {
+ var behavior = new UserStoppedTypingBehavior
+ {
+ StoppedTypingTimeThreshold = 500,
+ MinimumLengthThreshold = 3,
+ ShouldDismissKeyboardAutomatically = true
+ };
+
+ Assert.Equal(500, behavior.StoppedTypingTimeThreshold);
+ Assert.Equal(3, behavior.MinimumLengthThreshold);
+ Assert.True(behavior.ShouldDismissKeyboardAutomatically);
+ }
+}
+
+public class EventToCommandBehaviorTests
+{
+ [Fact]
+ public void EventToCommandBehavior_DefaultEventName_IsNull()
+ {
+ var behavior = new EventToCommandBehavior();
+
+ Assert.Null(behavior.EventName);
+ }
+
+ [Fact]
+ public void EventToCommandBehavior_DefaultCommand_IsNull()
+ {
+ var behavior = new EventToCommandBehavior();
+
+ Assert.Null(behavior.Command);
+ }
+
+ [Fact]
+ public void EventToCommandBehavior_DefaultCommandParameter_IsNull()
+ {
+ var behavior = new EventToCommandBehavior();
+
+ Assert.Null(behavior.CommandParameter);
+ }
+
+ [Fact]
+ public void EventToCommandBehavior_DefaultEventArgsConverter_IsNull()
+ {
+ var behavior = new EventToCommandBehavior();
+
+ Assert.Null(behavior.EventArgsConverter);
+ }
+
+ [Fact]
+ public void EventToCommandBehavior_CanSetEventName()
+ {
+ var behavior = new EventToCommandBehavior
+ {
+ EventName = "Clicked"
+ };
+
+ Assert.Equal("Clicked", behavior.EventName);
+ }
+}
+
+public class ImpliedOrderGridBehaviorTests
+{
+ [Fact]
+ public void ImpliedOrderGridBehavior_DefaultThrowOnLayoutWarning_IsFalse()
+ {
+ var behavior = new ImpliedOrderGridBehavior();
+
+ Assert.False(behavior.ThrowOnLayoutWarning);
+ }
+
+ [Fact]
+ public void ImpliedOrderGridBehavior_CanSetThrowOnLayoutWarning()
+ {
+ var behavior = new ImpliedOrderGridBehavior
+ {
+ ThrowOnLayoutWarning = true
+ };
+
+ Assert.True(behavior.ThrowOnLayoutWarning);
+ }
+}
+
+public class ProgressBarAnimationBehaviorTests
+{
+ [Fact]
+ public void ProgressBarAnimationBehavior_DefaultLength()
+ {
+ var behavior = new ProgressBarAnimationBehavior();
+
+ // ProgressBarAnimationBehaviorDefaults.Length is 500
+ Assert.Equal(500u, behavior.Length);
+ }
+
+ [Fact]
+ public void ProgressBarAnimationBehavior_DefaultEasing()
+ {
+ var behavior = new ProgressBarAnimationBehavior();
+
+ Assert.Equal(Easing.Linear, behavior.Easing);
+ }
+
+ [Fact]
+ public void ProgressBarAnimationBehavior_CanSetLength()
+ {
+ var behavior = new ProgressBarAnimationBehavior
+ {
+ Length = 500u
+ };
+
+ Assert.Equal(500u, behavior.Length);
+ }
+
+ [Fact]
+ public void ProgressBarAnimationBehavior_CanSetEasing()
+ {
+ var behavior = new ProgressBarAnimationBehavior
+ {
+ Easing = Easing.CubicOut
+ };
+
+ Assert.Equal(Easing.CubicOut, behavior.Easing);
+ }
+}
+
+public class AnimationBehaviorTests
+{
+ [Fact]
+ public void AnimationBehavior_DefaultAnimationType_IsNull()
+ {
+ var behavior = new AnimationBehavior();
+
+ Assert.Null(behavior.AnimationType);
+ }
+
+ [Fact]
+ public void AnimationBehavior_CanSetAnimationType()
+ {
+ var animation = new CommunityToolkit.Maui.Animations.FadeAnimation();
+ var behavior = new AnimationBehavior
+ {
+ AnimationType = animation
+ };
+
+ Assert.Same(animation, behavior.AnimationType);
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/ExtensionsAndEventArgsTests.cs b/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/ExtensionsAndEventArgsTests.cs
new file mode 100644
index 0000000000..099126713a
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/ExtensionsAndEventArgsTests.cs
@@ -0,0 +1,425 @@
+using System.Reflection;
+using CommunityToolkit.Maui.Converters;
+using CommunityToolkit.Maui.Core;
+using CommunityToolkit.Maui.Core.Views;
+using CommunityToolkit.Maui.Extensions;
+using CommunityToolkit.Maui.Views;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace CommunityToolkit.Maui.DeviceTests.Tests.Additional;
+
+#region MauiDrawingLineCompletedEventArgs Tests
+
+public class MauiDrawingLineCompletedEventArgsTests
+{
+ [Fact]
+ public void MauiDrawingLineCompletedEventArgs_CarriesLine()
+ {
+ var line = new MauiDrawingLine();
+ var args = new MauiDrawingLineCompletedEventArgs(line);
+
+ Assert.Same(line, args.Line);
+ }
+
+ [Fact]
+ public void MauiDrawingLineCompletedEventArgs_IsEventArgs()
+ {
+ var line = new MauiDrawingLine();
+ var args = new MauiDrawingLineCompletedEventArgs(line);
+
+ Assert.IsAssignableFrom(args);
+ }
+}
+
+#endregion
+
+#region NavigationEventArgsExtensions Tests
+
+public class NavigationEventArgsExtensionsTests
+{
+ [Fact]
+ public void NavigationEventArgsExtensions_MethodsExist()
+ {
+ var type = typeof(NavigationEventArgsExtensions);
+ var methods = type.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
+
+ Assert.Contains(methods, m => m.Name == "IsDestinationPageACommunityToolkitPopupPage");
+ Assert.Contains(methods, m => m.Name == "WasPreviousPageACommunityToolkitPopupPage");
+ }
+
+ [Fact]
+ public void NavigationEventArgsExtensions_IsDestinationPageACommunityToolkitPopupPage_HasNavigatedFromOverload()
+ {
+ var method = typeof(NavigationEventArgsExtensions).GetMethod("IsDestinationPageACommunityToolkitPopupPage", [typeof(NavigatedFromEventArgs)]);
+ Assert.NotNull(method);
+ Assert.Equal(typeof(bool), method.ReturnType);
+ }
+
+ [Fact]
+ public void NavigationEventArgsExtensions_WasPreviousPageACommunityToolkitPopupPage_HasNavigatedToOverload()
+ {
+ var method = typeof(NavigationEventArgsExtensions).GetMethod("WasPreviousPageACommunityToolkitPopupPage", [typeof(NavigatedToEventArgs)]);
+ Assert.NotNull(method);
+ Assert.Equal(typeof(bool), method.ReturnType);
+ }
+
+ [Fact]
+ public void NavigationEventArgsExtensions_IsDestinationPageACommunityToolkitPopupPage_HasNavigatingFromOverload()
+ {
+ var method = typeof(NavigationEventArgsExtensions).GetMethod("IsDestinationPageACommunityToolkitPopupPage", [typeof(NavigatingFromEventArgs)]);
+ Assert.NotNull(method);
+ Assert.Equal(typeof(bool), method.ReturnType);
+ }
+}
+
+#endregion
+
+#region ServiceCollectionExtensions Tests
+
+public partial class ServiceCollectionExtensionsTests
+{
+ [Fact]
+ public void AddTransientPopup_RegistersPopup()
+ {
+ var services = new ServiceCollection();
+ var result = services.AddTransientPopup();
+
+ Assert.Same(services, result);
+ Assert.Contains(services, sd => sd.ServiceType == typeof(Popup));
+ }
+
+ [Fact]
+ public void AddSingletonPopup_RegistersPopup()
+ {
+ var services = new ServiceCollection();
+ var result = services.AddSingletonPopup();
+
+ Assert.Same(services, result);
+ Assert.Contains(services, sd => sd.ServiceType == typeof(Popup));
+ }
+
+ [Fact]
+ public void AddTransientPopup_WithViewModel_RegistersBoth()
+ {
+ var services = new ServiceCollection();
+ var result = services.AddTransientPopup();
+
+ Assert.Same(services, result);
+ Assert.Contains(services, sd => sd.ServiceType == typeof(Popup));
+ Assert.Contains(services, sd => sd.ServiceType == typeof(TestPopupViewModel));
+ }
+
+ [Fact]
+ public void AddSingletonPopup_WithViewModel_RegistersBoth()
+ {
+ var services = new ServiceCollection();
+ var result = services.AddSingletonPopup();
+
+ Assert.Same(services, result);
+ Assert.Contains(services, sd => sd.ServiceType == typeof(Popup));
+ Assert.Contains(services, sd => sd.ServiceType == typeof(TestPopupViewModel));
+ }
+
+ partial class TestPopupViewModel : System.ComponentModel.INotifyPropertyChanged
+ {
+#pragma warning disable CS0067 // Event is required by INotifyPropertyChanged but never raised in this test stub
+ public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;
+#pragma warning restore CS0067
+ }
+}
+
+#endregion
+
+#region AppThemeObjectExtensions Tests
+
+public class AppThemeObjectExtensionsTests
+{
+ [Fact]
+ public void SetAppThemeColor_MethodExists()
+ {
+ var method = typeof(AppThemeObjectExtensions).GetMethod("SetAppThemeColor", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
+ Assert.NotNull(method);
+
+ var parameters = method.GetParameters();
+ Assert.Equal(3, parameters.Length);
+ Assert.Equal(typeof(BindableObject), parameters[0].ParameterType);
+ Assert.Equal(typeof(BindableProperty), parameters[1].ParameterType);
+ Assert.Equal(typeof(AppThemeColor), parameters[2].ParameterType);
+ }
+
+ [Fact]
+ public void SetAppTheme_MethodExists()
+ {
+ var method = typeof(AppThemeObjectExtensions).GetMethod("SetAppTheme", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
+ Assert.NotNull(method);
+ Assert.True(method.IsGenericMethod);
+ }
+
+ [Fact]
+ public void SetAppThemeColor_DoesNotThrow()
+ {
+ var label = new Label();
+ var appThemeColor = new AppThemeColor
+ {
+ Light = Colors.White,
+ Dark = Colors.Black,
+ };
+
+ // Should not throw
+ label.SetAppThemeColor(Label.TextColorProperty, appThemeColor);
+ Assert.True(true);
+ }
+}
+
+#endregion
+
+#region MultiValueConverterExtension Tests
+
+public partial class MultiValueConverterExtensionTests
+{
+ class TestMultiConverter : MultiValueConverterExtension, ICommunityToolkitMultiValueConverter
+ {
+ public object? Convert(object?[]? values, Type targetType, object? parameter, System.Globalization.CultureInfo? culture)
+ {
+ return values?.Length > 0 ? values[0] : null;
+ }
+
+ public object[]? ConvertBack(object? value, Type[] targetTypes, object? parameter, System.Globalization.CultureInfo? culture)
+ {
+ return value is null ? null : [value];
+ }
+ }
+
+ [Fact]
+ public void MultiValueConverterExtension_ProvideValue_ReturnsSelf()
+ {
+ var converter = new TestMultiConverter();
+ var result = converter.ProvideValue(new TestServiceProvider());
+
+ Assert.Same(converter, result);
+ }
+
+ [Fact]
+ public void MultiValueConverterExtension_IsIMarkupExtension()
+ {
+ var converter = new TestMultiConverter();
+ Assert.IsAssignableFrom>(converter);
+ }
+
+ partial class TestServiceProvider : IServiceProvider
+ {
+ public object? GetService(Type serviceType) => null;
+ }
+}
+
+#endregion
+
+#region NullableExtensions Tests (internal, via reflection)
+
+public class NullableExtensionsTests
+{
+ // NullableExtensions lives in CommunityToolkit.Maui.Core, not CommunityToolkit.Maui
+ static readonly Type nullableExtensionsType = typeof(CommunityToolkit.Maui.Core.Extensions.ColorConversionExtensions).Assembly
+ .GetType("CommunityToolkit.Maui.Core.Extensions.NullableExtensions")
+ ?? throw new InvalidOperationException("NullableExtensions type not found");
+
+ static readonly MethodInfo isNullableMethod = nullableExtensionsType
+ .GetMethod("IsNullable", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
+ ?? throw new InvalidOperationException("IsNullable method not found");
+
+ [Fact]
+ public void IsNullable_ReferenceType_ReturnsTrue()
+ {
+ var result = isNullableMethod.Invoke(null, [typeof(string)]);
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void IsNullable_NullableValueType_ReturnsTrue()
+ {
+ var result = isNullableMethod.Invoke(null, [typeof(int?)]);
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void IsNullable_ValueType_ReturnsFalse()
+ {
+ var result = isNullableMethod.Invoke(null, [typeof(int)]);
+ Assert.Equal(false, result);
+ }
+
+ [Fact]
+ public void IsNullable_Bool_ReturnsFalse()
+ {
+ var result = isNullableMethod.Invoke(null, [typeof(bool)]);
+ Assert.Equal(false, result);
+ }
+
+ [Fact]
+ public void IsNullable_NullableBool_ReturnsTrue()
+ {
+ var result = isNullableMethod.Invoke(null, [typeof(bool?)]);
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void IsNullable_Object_ReturnsTrue()
+ {
+ var result = isNullableMethod.Invoke(null, [typeof(object)]);
+ Assert.Equal(true, result);
+ }
+}
+
+#endregion
+
+#region PropertyChangedEventArgsExtensions Tests
+
+public class PropertyChangedEventArgsExtensionsTests
+{
+ [Fact]
+ public void IsOneOf_MatchingProperty_ReturnsTrue()
+ {
+ var result = "Text".IsOneOf(Label.TextProperty);
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void IsOneOf_NonMatchingProperty_ReturnsFalse()
+ {
+ var result = "TextColor".IsOneOf(Label.TextProperty);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void IsOneOf_MultipleProperties_MatchesOne_ReturnsTrue()
+ {
+ var result = "FontSize".IsOneOf(Label.TextProperty, Label.FontSizeProperty);
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void IsOneOf_EmptyProperties_ReturnsFalse()
+ {
+ var result = "Text".IsOneOf();
+ Assert.False(result);
+ }
+}
+
+#endregion
+
+#region PopupExtensions Tests
+
+public class PopupExtensionsTests
+{
+ [Fact]
+ public void PopupExtensions_ShowPopup_MethodExists()
+ {
+ var type = typeof(PopupExtensions);
+ var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Static);
+
+ Assert.Contains(methods, m => m.Name == "ShowPopup");
+ }
+
+ [Fact]
+ public void PopupExtensions_ShowPopupAsync_MethodExists()
+ {
+ var type = typeof(PopupExtensions);
+ var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Static);
+
+ Assert.Contains(methods, m => m.Name == "ShowPopupAsync");
+ }
+
+ [Fact]
+ public void PopupExtensions_ClosePopupAsync_MethodExists()
+ {
+ var type = typeof(PopupExtensions);
+ var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Static);
+
+ // The public API is ClosePopupAsync, not ClosePopup
+ Assert.Contains(methods, m => m.Name == "ClosePopupAsync");
+ }
+}
+
+#endregion
+
+#region ColorAnimationExtensions Tests
+
+public class ColorAnimationExtensionsTests
+{
+ [Fact]
+ public void BackgroundColorTo_MethodExists()
+ {
+ var method = typeof(ColorAnimationExtensions).GetMethod("BackgroundColorTo", BindingFlags.Public | BindingFlags.Static);
+ Assert.NotNull(method);
+ }
+
+ [Fact]
+ public void BackgroundColorTo_HasCorrectParameters()
+ {
+ var method = typeof(ColorAnimationExtensions).GetMethod("BackgroundColorTo", BindingFlags.Public | BindingFlags.Static);
+ Assert.NotNull(method);
+
+ var parameters = method.GetParameters();
+ Assert.Equal(6, parameters.Length);
+ Assert.Equal(typeof(VisualElement), parameters[0].ParameterType);
+ Assert.Equal(typeof(Color), parameters[1].ParameterType);
+ Assert.Equal(typeof(uint), parameters[2].ParameterType);
+ Assert.Equal(typeof(uint), parameters[3].ParameterType);
+ Assert.Equal(typeof(Easing), parameters[4].ParameterType);
+ Assert.Equal(typeof(CancellationToken), parameters[5].ParameterType);
+ }
+
+ [Fact]
+ public void BackgroundColorTo_DefaultParameters_AreCorrect()
+ {
+ var method = typeof(ColorAnimationExtensions).GetMethod("BackgroundColorTo", BindingFlags.Public | BindingFlags.Static);
+ Assert.NotNull(method);
+
+ var parameters = method.GetParameters();
+ Assert.Equal(16u, parameters[2].DefaultValue); // rate
+ Assert.Equal(250u, parameters[3].DefaultValue); // length
+ Assert.Null(parameters[4].DefaultValue); // easing
+ }
+}
+
+#endregion
+
+#region AppThemeResourceExtension Tests
+
+public class AppThemeResourceExtensionTests
+{
+ [Fact]
+ public void AppThemeResourceExtension_CanBeCreated()
+ {
+ var extension = new AppThemeResourceExtension();
+ Assert.NotNull(extension);
+ }
+
+ [Fact]
+ public void AppThemeResourceExtension_Key_DefaultIsNull()
+ {
+ var extension = new AppThemeResourceExtension();
+ Assert.Null(extension.Key);
+ }
+
+ [Fact]
+ public void AppThemeResourceExtension_SetKey_UpdatesValue()
+ {
+ var extension = new AppThemeResourceExtension
+ {
+ Key = "TestKey",
+ };
+
+ Assert.Equal("TestKey", extension.Key);
+ }
+
+ [Fact]
+ public void AppThemeResourceExtension_IsIMarkupExtension()
+ {
+ var extension = new AppThemeResourceExtension();
+ Assert.IsAssignableFrom>(extension);
+ }
+}
+
+#endregion
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/PlatformBehaviorsAndServicesTests.cs b/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/PlatformBehaviorsAndServicesTests.cs
new file mode 100644
index 0000000000..0d9c2fc1cd
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Tests/Additional/PlatformBehaviorsAndServicesTests.cs
@@ -0,0 +1,1022 @@
+using System.Reflection;
+using CommunityToolkit.Maui.Animations;
+using CommunityToolkit.Maui.Behaviors;
+using CommunityToolkit.Maui.Converters;
+using CommunityToolkit.Maui.Core;
+using CommunityToolkit.Maui.Services;
+using CommunityToolkit.Maui.Views;
+using Microsoft.Maui.ApplicationModel;
+using Xunit;
+
+namespace CommunityToolkit.Maui.DeviceTests.Tests.Additional;
+
+#region TouchBehavior Tests
+
+public class TouchBehaviorTests
+{
+ [Fact]
+ public void TouchBehavior_DefaultValues_AreCorrect()
+ {
+ var behavior = new TouchBehavior();
+
+ Assert.True(behavior.IsEnabled);
+ Assert.True(behavior.ShouldMakeChildrenInputTransparent);
+ Assert.Null(behavior.Command);
+ Assert.Null(behavior.CommandParameter);
+ Assert.Null(behavior.LongPressCommand);
+ Assert.Null(behavior.LongPressCommandParameter);
+ Assert.Equal(500, behavior.LongPressDuration);
+ Assert.Equal(TouchStatus.Completed, behavior.CurrentTouchStatus);
+ Assert.Equal(TouchState.Default, behavior.CurrentTouchState);
+ Assert.Equal(TouchInteractionStatus.Completed, behavior.CurrentInteractionStatus);
+ Assert.Equal(HoverStatus.Exited, behavior.CurrentHoverStatus);
+ Assert.Equal(HoverState.Default, behavior.CurrentHoverState);
+ Assert.Null(behavior.DefaultBackgroundColor);
+ Assert.Null(behavior.HoveredBackgroundColor);
+ Assert.Null(behavior.PressedBackgroundColor);
+ Assert.Null(behavior.DefaultOpacity);
+ Assert.Null(behavior.HoveredOpacity);
+ Assert.Null(behavior.PressedOpacity);
+ Assert.Null(behavior.DefaultScale);
+ Assert.Null(behavior.HoveredScale);
+ Assert.Null(behavior.PressedScale);
+ Assert.Null(behavior.DefaultTranslationX);
+ Assert.Null(behavior.HoveredTranslationX);
+ Assert.Null(behavior.PressedTranslationX);
+ Assert.Null(behavior.DefaultTranslationY);
+ Assert.Null(behavior.HoveredTranslationY);
+ Assert.Null(behavior.PressedTranslationY);
+ Assert.Null(behavior.DefaultRotation);
+ Assert.Null(behavior.HoveredRotation);
+ Assert.Null(behavior.PressedRotation);
+ Assert.Null(behavior.DefaultRotationX);
+ Assert.Null(behavior.HoveredRotationX);
+ Assert.Null(behavior.PressedRotationX);
+ Assert.Null(behavior.DefaultRotationY);
+ Assert.Null(behavior.HoveredRotationY);
+ Assert.Null(behavior.PressedRotationY);
+ Assert.Null(behavior.PressedAnimationDuration);
+ Assert.Null(behavior.PressedAnimationEasing);
+ Assert.Null(behavior.DefaultAnimationDuration);
+ Assert.Null(behavior.DefaultAnimationEasing);
+ Assert.Null(behavior.HoveredAnimationDuration);
+ Assert.Null(behavior.HoveredAnimationEasing);
+ Assert.Equal(0, behavior.DisallowTouchThreshold);
+ }
+
+ [Fact]
+ public void TouchBehavior_VisualStateConstants_AreCorrect()
+ {
+ Assert.Equal("Unpressed", TouchBehavior.UnpressedVisualState);
+ Assert.Equal("Pressed", TouchBehavior.PressedVisualState);
+ Assert.Equal("Hovered", TouchBehavior.HoveredVisualState);
+ }
+
+ [Fact]
+ public void TouchBehavior_SetProperties_UpdatesValues()
+ {
+ var behavior = new TouchBehavior
+ {
+ IsEnabled = false,
+ ShouldMakeChildrenInputTransparent = false,
+ LongPressDuration = 1000,
+ DisallowTouchThreshold = 5,
+ DefaultBackgroundColor = Colors.Red,
+ HoveredBackgroundColor = Colors.Blue,
+ PressedBackgroundColor = Colors.Green,
+ DefaultOpacity = 0.5,
+ HoveredOpacity = 0.7,
+ PressedOpacity = 0.9,
+ DefaultScale = 1.0,
+ HoveredScale = 1.1,
+ PressedScale = 0.9,
+ DefaultTranslationX = 10,
+ HoveredTranslationX = 20,
+ PressedTranslationX = 30,
+ DefaultTranslationY = 5,
+ HoveredTranslationY = 15,
+ PressedTranslationY = 25,
+ DefaultRotation = 0,
+ HoveredRotation = 45,
+ PressedRotation = 90,
+ DefaultRotationX = 0,
+ HoveredRotationX = 10,
+ PressedRotationX = 20,
+ DefaultRotationY = 0,
+ HoveredRotationY = 10,
+ PressedRotationY = 20,
+ PressedAnimationDuration = 100,
+ PressedAnimationEasing = Easing.CubicIn,
+ DefaultAnimationDuration = 200,
+ DefaultAnimationEasing = Easing.CubicOut,
+ HoveredAnimationDuration = 150,
+ HoveredAnimationEasing = Easing.Linear,
+ };
+
+ Assert.False(behavior.IsEnabled);
+ Assert.False(behavior.ShouldMakeChildrenInputTransparent);
+ Assert.Equal(1000, behavior.LongPressDuration);
+ Assert.Equal(5, behavior.DisallowTouchThreshold);
+ Assert.Equal(Colors.Red, behavior.DefaultBackgroundColor);
+ Assert.Equal(Colors.Blue, behavior.HoveredBackgroundColor);
+ Assert.Equal(Colors.Green, behavior.PressedBackgroundColor);
+ Assert.Equal(0.5, behavior.DefaultOpacity);
+ Assert.Equal(0.7, behavior.HoveredOpacity);
+ Assert.Equal(0.9, behavior.PressedOpacity);
+ Assert.Equal(1.0, behavior.DefaultScale);
+ Assert.Equal(1.1, behavior.HoveredScale);
+ Assert.Equal(0.9, behavior.PressedScale);
+ Assert.Equal(10, behavior.DefaultTranslationX);
+ Assert.Equal(20, behavior.HoveredTranslationX);
+ Assert.Equal(30, behavior.PressedTranslationX);
+ Assert.Equal(5, behavior.DefaultTranslationY);
+ Assert.Equal(15, behavior.HoveredTranslationY);
+ Assert.Equal(25, behavior.PressedTranslationY);
+ Assert.Equal(0, behavior.DefaultRotation);
+ Assert.Equal(45, behavior.HoveredRotation);
+ Assert.Equal(90, behavior.PressedRotation);
+ Assert.Equal(0, behavior.DefaultRotationX);
+ Assert.Equal(10, behavior.HoveredRotationX);
+ Assert.Equal(20, behavior.PressedRotationX);
+ Assert.Equal(0, behavior.DefaultRotationY);
+ Assert.Equal(10, behavior.HoveredRotationY);
+ Assert.Equal(20, behavior.PressedRotationY);
+ Assert.Equal(100, behavior.PressedAnimationDuration);
+ Assert.Equal(Easing.CubicIn, behavior.PressedAnimationEasing);
+ Assert.Equal(200, behavior.DefaultAnimationDuration);
+ Assert.Equal(Easing.CubicOut, behavior.DefaultAnimationEasing);
+ Assert.Equal(150, behavior.HoveredAnimationDuration);
+ Assert.Equal(Easing.Linear, behavior.HoveredAnimationEasing);
+ }
+
+ [Fact]
+ public void TouchBehavior_Command_CanBeSet()
+ {
+ var command = new Command(() => { });
+ var behavior = new TouchBehavior
+ {
+ Command = command,
+ CommandParameter = "test",
+ };
+
+ Assert.Same(command, behavior.Command);
+ Assert.Equal("test", behavior.CommandParameter);
+ }
+
+ [Fact]
+ public void TouchBehavior_LongPressCommand_CanBeSet()
+ {
+ var command = new Command(() => { });
+ var behavior = new TouchBehavior
+ {
+ LongPressCommand = command,
+ LongPressCommandParameter = 42,
+ };
+
+ Assert.Same(command, behavior.LongPressCommand);
+ Assert.Equal(42, behavior.LongPressCommandParameter);
+ }
+
+ [Fact]
+ public void TouchBehavior_Events_CanSubscribeAndUnsubscribe()
+ {
+ var behavior = new TouchBehavior();
+ var touchStatusFired = false;
+ var touchStateFired = false;
+ var interactionFired = false;
+ var hoverStatusFired = false;
+ var hoverStateFired = false;
+ var gestureFired = false;
+ var longPressFired = false;
+
+ EventHandler touchStatusHandler = (s, e) => touchStatusFired = true;
+ EventHandler touchStateHandler = (s, e) => touchStateFired = true;
+ EventHandler interactionHandler = (s, e) => interactionFired = true;
+ EventHandler hoverStatusHandler = (s, e) => hoverStatusFired = true;
+ EventHandler hoverStateHandler = (s, e) => hoverStateFired = true;
+ EventHandler gestureHandler = (s, e) => gestureFired = true;
+ EventHandler longPressHandler = (s, e) => longPressFired = true;
+
+ behavior.CurrentTouchStatusChanged += touchStatusHandler;
+ behavior.CurrentTouchStateChanged += touchStateHandler;
+ behavior.InteractionStatusChanged += interactionHandler;
+ behavior.HoverStatusChanged += hoverStatusHandler;
+ behavior.HoverStateChanged += hoverStateHandler;
+ behavior.TouchGestureCompleted += gestureHandler;
+ behavior.LongPressCompleted += longPressHandler;
+
+ // Unsubscribe
+ behavior.CurrentTouchStatusChanged -= touchStatusHandler;
+ behavior.CurrentTouchStateChanged -= touchStateHandler;
+ behavior.InteractionStatusChanged -= interactionHandler;
+ behavior.HoverStatusChanged -= hoverStatusHandler;
+ behavior.HoverStateChanged -= hoverStateHandler;
+ behavior.TouchGestureCompleted -= gestureHandler;
+ behavior.LongPressCompleted -= longPressHandler;
+
+ // Verify no exceptions during subscribe/unsubscribe
+ Assert.False(touchStatusFired);
+ Assert.False(touchStateFired);
+ Assert.False(interactionFired);
+ Assert.False(hoverStatusFired);
+ Assert.False(hoverStateFired);
+ Assert.False(gestureFired);
+ Assert.False(longPressFired);
+ }
+}
+
+#endregion
+
+#region ImageTouchBehavior Tests
+
+public class ImageTouchBehaviorTests
+{
+ [Fact]
+ public void ImageTouchBehavior_DefaultValues_AreCorrect()
+ {
+ var behavior = new ImageTouchBehavior();
+
+ Assert.Null(behavior.DefaultImageSource);
+ Assert.Null(behavior.HoveredImageSource);
+ Assert.Null(behavior.PressedImageSource);
+ Assert.Null(behavior.DefaultImageAspect);
+ Assert.Null(behavior.HoveredImageAspect);
+ Assert.Null(behavior.PressedImageAspect);
+ Assert.False(behavior.ShouldSetImageOnAnimationEnd);
+ }
+
+ [Fact]
+ public void ImageTouchBehavior_SetProperties_UpdatesValues()
+ {
+ var defaultSource = ImageSource.FromFile("default.png");
+ var hoveredSource = ImageSource.FromFile("hovered.png");
+ var pressedSource = ImageSource.FromFile("pressed.png");
+
+ var behavior = new ImageTouchBehavior
+ {
+ DefaultImageSource = defaultSource,
+ HoveredImageSource = hoveredSource,
+ PressedImageSource = pressedSource,
+ DefaultImageAspect = Aspect.AspectFit,
+ HoveredImageAspect = Aspect.AspectFill,
+ PressedImageAspect = Aspect.Fill,
+ ShouldSetImageOnAnimationEnd = true,
+ };
+
+ Assert.Same(defaultSource, behavior.DefaultImageSource);
+ Assert.Same(hoveredSource, behavior.HoveredImageSource);
+ Assert.Same(pressedSource, behavior.PressedImageSource);
+ Assert.Equal(Aspect.AspectFit, behavior.DefaultImageAspect);
+ Assert.Equal(Aspect.AspectFill, behavior.HoveredImageAspect);
+ Assert.Equal(Aspect.Fill, behavior.PressedImageAspect);
+ Assert.True(behavior.ShouldSetImageOnAnimationEnd);
+ }
+
+ [Fact]
+ public void ImageTouchBehavior_InheritsFromTouchBehavior()
+ {
+ var behavior = new ImageTouchBehavior();
+ Assert.IsAssignableFrom(behavior);
+ }
+}
+
+#endregion
+
+#region IconTintColorBehavior Tests
+
+public class IconTintColorBehaviorTests
+{
+ [Fact]
+ public void IconTintColorBehavior_DefaultTintColor_IsNull()
+ {
+ var behavior = new IconTintColorBehavior();
+ Assert.Null(behavior.TintColor);
+ }
+
+ [Fact]
+ public void IconTintColorBehavior_SetTintColor_UpdatesValue()
+ {
+ var behavior = new IconTintColorBehavior
+ {
+ TintColor = Colors.Red,
+ };
+
+ Assert.Equal(Colors.Red, behavior.TintColor);
+ }
+
+ [Fact]
+ public void IconTintColorBehavior_SetTintColorToNull_Works()
+ {
+ var behavior = new IconTintColorBehavior
+ {
+ TintColor = Colors.Blue,
+ };
+
+ behavior.TintColor = null;
+ Assert.Null(behavior.TintColor);
+ }
+
+ [Fact]
+ public void IconTintColorBehavior_TintColorBindableProperty_ReadsBackCorrectValue()
+ {
+ var behavior = new IconTintColorBehavior();
+ var expectedColor = Colors.Fuchsia;
+ behavior.TintColor = expectedColor;
+
+ var appliedColor = behavior.GetValue(IconTintColorBehavior.TintColorProperty) as Color;
+
+ Assert.Equal(expectedColor, appliedColor);
+ }
+}
+
+#endregion
+
+#region StatusBarApplyOn Enum Tests
+
+public class StatusBarApplyOnTests
+{
+ [Theory]
+ [InlineData(StatusBarApplyOn.OnBehaviorAttachedTo, 0)]
+ [InlineData(StatusBarApplyOn.OnPageNavigatedTo, 1)]
+ public void StatusBarApplyOn_HasExpectedValues(StatusBarApplyOn applyOn, int expected)
+ {
+ Assert.Equal(expected, (int)applyOn);
+ }
+
+ [Fact]
+ public void StatusBarApplyOn_HasTwoValues()
+ {
+ var values = Enum.GetValues();
+ Assert.Equal(2, values.Length);
+ }
+}
+
+#endregion
+
+#region StatusBarBehavior Tests
+
+public class StatusBarBehaviorTests
+{
+ [Fact]
+ public void StatusBarBehavior_DefaultStatusBarColor_IsTransparent()
+ {
+ var behavior = new StatusBarBehavior();
+ Assert.Equal(Colors.Transparent, behavior.StatusBarColor);
+ }
+
+ [Fact]
+ public void StatusBarBehavior_DefaultStatusBarStyle_IsDefault()
+ {
+ var behavior = new StatusBarBehavior();
+ Assert.Equal(StatusBarStyle.Default, behavior.StatusBarStyle);
+ }
+
+ [Fact]
+ public void StatusBarBehavior_DefaultApplyOn_IsOnBehaviorAttachedTo()
+ {
+ var behavior = new StatusBarBehavior();
+ Assert.Equal(StatusBarApplyOn.OnBehaviorAttachedTo, behavior.ApplyOn);
+ }
+
+ [Fact]
+ public async Task StatusBarBehavior_SetStatusBarColor_UpdatesValue()
+ {
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ var behavior = new StatusBarBehavior
+ {
+ StatusBarColor = Colors.Fuchsia,
+ };
+
+ Assert.Equal(Colors.Fuchsia, behavior.StatusBarColor);
+ });
+ }
+
+ [Fact]
+ public async Task StatusBarBehavior_SetStatusBarStyle_UpdatesValue()
+ {
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ var behavior = new StatusBarBehavior
+ {
+ StatusBarStyle = StatusBarStyle.LightContent,
+ };
+
+ Assert.Equal(StatusBarStyle.LightContent, behavior.StatusBarStyle);
+ });
+ }
+
+ [Fact]
+ public void StatusBarBehavior_SetApplyOn_OnPageNavigatedTo_UpdatesValue()
+ {
+ var behavior = new StatusBarBehavior
+ {
+ ApplyOn = StatusBarApplyOn.OnPageNavigatedTo,
+ };
+
+ Assert.Equal(StatusBarApplyOn.OnPageNavigatedTo, behavior.ApplyOn);
+ }
+
+ [Fact]
+ public void StatusBarBehavior_IsBasePlatformBehaviorOfPage()
+ {
+ var behavior = new StatusBarBehavior();
+ Assert.IsAssignableFrom>(behavior);
+ }
+
+ [Fact]
+ public async Task StatusBarBehavior_CanBeAttachedToPage()
+ {
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ var page = new ContentPage();
+ var behavior = new StatusBarBehavior
+ {
+ StatusBarColor = Colors.Fuchsia,
+ };
+
+ page.Behaviors.Add(behavior);
+
+ var attachedBehavior = page.Behaviors.FirstOrDefault(x => x is StatusBarBehavior);
+ Assert.NotNull(attachedBehavior);
+ Assert.Same(behavior, attachedBehavior);
+ });
+ }
+
+ [Fact]
+ public async Task StatusBarBehavior_AttachedToPage_ColorIsPreserved()
+ {
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ var page = new ContentPage();
+ var behavior = new StatusBarBehavior
+ {
+ StatusBarColor = Colors.Fuchsia,
+ };
+
+ page.Behaviors.Add(behavior);
+
+ var attachedBehavior = page.Behaviors.OfType().FirstOrDefault();
+ Assert.NotNull(attachedBehavior);
+ Assert.Equal(Colors.Fuchsia, attachedBehavior.StatusBarColor);
+ });
+ }
+
+ [Fact]
+ public void StatusBarBehavior_CanBeDetachedFromPage()
+ {
+ var page = new ContentPage();
+ var behavior = new StatusBarBehavior();
+
+ page.Behaviors.Add(behavior);
+ Assert.Single(page.Behaviors.OfType());
+
+ page.Behaviors.Remove(behavior);
+ Assert.Empty(page.Behaviors.OfType());
+ }
+
+ [Fact]
+ public async Task StatusBarBehavior_MultipleBehaviors_CanBeAttached()
+ {
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ var page = new ContentPage();
+ var behavior1 = new StatusBarBehavior { StatusBarColor = Colors.Red };
+ var behavior2 = new StatusBarBehavior { StatusBarColor = Colors.Blue };
+
+ page.Behaviors.Add(behavior1);
+ page.Behaviors.Add(behavior2);
+
+ var behaviors = page.Behaviors.OfType().ToList();
+ Assert.Equal(2, behaviors.Count);
+ Assert.Equal(Colors.Red, behaviors[0].StatusBarColor);
+ Assert.Equal(Colors.Blue, behaviors[1].StatusBarColor);
+ });
+ }
+}
+
+#endregion
+
+#region SetFocusOnEntryCompletedBehavior Tests
+
+public class SetFocusOnEntryCompletedBehaviorTests
+{
+ [Fact]
+ public void SetFocusOnEntryCompletedBehavior_CanBeCreated()
+ {
+ var behavior = new SetFocusOnEntryCompletedBehavior();
+ Assert.NotNull(behavior);
+ }
+
+ [Fact]
+ public void SetFocusOnEntryCompletedBehavior_NextElementProperty_Exists()
+ {
+ var property = typeof(SetFocusOnEntryCompletedBehavior).GetField("NextElementProperty", BindingFlags.Public | BindingFlags.Static);
+ Assert.NotNull(property);
+ }
+
+ [Fact]
+ public void SetFocusOnEntryCompletedBehavior_SetNextElement_Works()
+ {
+ var entry1 = new Entry();
+ var entry2 = new Entry();
+
+ SetFocusOnEntryCompletedBehavior.SetNextElement(entry1, entry2);
+ var result = SetFocusOnEntryCompletedBehavior.GetNextElement(entry1);
+
+ Assert.Same(entry2, result);
+ }
+
+ [Fact]
+ public void SetFocusOnEntryCompletedBehavior_GetNextElement_DefaultIsNull()
+ {
+ var entry = new Entry();
+ var result = SetFocusOnEntryCompletedBehavior.GetNextElement(entry);
+ Assert.Null(result);
+ }
+}
+
+#endregion
+
+#region BaseAnimation Tests
+
+public partial class BaseAnimationTests
+{
+ partial class TestAnimation : BaseAnimation
+ {
+ public override Task Animate(VisualElement view, CancellationToken token = default)
+ {
+ return Task.CompletedTask;
+ }
+ }
+
+ partial class TestAnimationWithLength : BaseAnimation
+ {
+ public TestAnimationWithLength(uint length) : base(length)
+ {
+ }
+
+ public override Task Animate(VisualElement view, CancellationToken token = default)
+ {
+ return Task.CompletedTask;
+ }
+ }
+
+ [Fact]
+ public void BaseAnimation_DefaultLength_Is250()
+ {
+ var animation = new TestAnimation();
+ Assert.Equal(250u, animation.Length);
+ }
+
+ [Fact]
+ public void BaseAnimation_DefaultEasing_IsLinear()
+ {
+ var animation = new TestAnimation();
+ Assert.Equal(Easing.Linear, animation.Easing);
+ }
+
+ [Fact]
+ public void BaseAnimation_CustomLength_IsRespected()
+ {
+ var animation = new TestAnimationWithLength(500);
+ Assert.Equal(500u, animation.Length);
+ }
+
+ [Fact]
+ public void BaseAnimation_SetLength_UpdatesValue()
+ {
+ var animation = new TestAnimation
+ {
+ Length = 1000,
+ };
+
+ Assert.Equal(1000u, animation.Length);
+ }
+
+ [Fact]
+ public void BaseAnimation_SetEasing_UpdatesValue()
+ {
+ var animation = new TestAnimation
+ {
+ Easing = Easing.CubicInOut,
+ };
+
+ Assert.Equal(Easing.CubicInOut, animation.Easing);
+ }
+
+ [Fact]
+ public async Task BaseAnimation_Animate_Completes()
+ {
+ var animation = new TestAnimation();
+ var label = new Label();
+
+ await animation.Animate(label);
+ // If we get here without exception, the test passes
+ Assert.True(true);
+ }
+}
+
+#endregion
+
+#region FadeAnimation Additional Tests
+
+public class FadeAnimationAdditionalTests
+{
+ [Fact]
+ public void FadeAnimation_DefaultOpacity_IsFadeAnimationDefault()
+ {
+ var animation = new FadeAnimation();
+
+ // Default opacity comes from FadeAnimationDefaults.Opacity (0.3)
+ Assert.Equal(0.3, animation.Opacity);
+ }
+
+ [Fact]
+ public void FadeAnimation_SetOpacity_UpdatesValue()
+ {
+ var animation = new FadeAnimation
+ {
+ Opacity = 0.5,
+ };
+
+ Assert.Equal(0.5, animation.Opacity);
+ }
+
+ [Fact]
+ public void FadeAnimation_InheritsFromBaseAnimation()
+ {
+ var animation = new FadeAnimation();
+ Assert.IsAssignableFrom(animation);
+ }
+}
+
+#endregion
+
+#region PopupService Tests
+
+public class PopupServiceTests
+{
+ [Fact]
+ public void PopupService_Constructor_WithServiceProvider_Works()
+ {
+ var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
+ var serviceProvider = services.BuildServiceProvider();
+
+ var popupService = new PopupService(serviceProvider);
+ Assert.NotNull(popupService);
+ }
+
+ [Fact]
+ public void PopupService_ImplementsIPopupService()
+ {
+ var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
+ var serviceProvider = services.BuildServiceProvider();
+
+ var popupService = new PopupService(serviceProvider);
+ Assert.IsAssignableFrom(popupService);
+ }
+
+ [Fact]
+ public void PopupService_ShowPopup_NullPage_ThrowsArgumentNullException()
+ {
+ var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
+ var serviceProvider = services.BuildServiceProvider();
+ var popupService = new PopupService(serviceProvider);
+
+ var thrown = false;
+ try
+ {
+ popupService.ShowPopup(default(Page)!);
+ }
+ catch (ArgumentNullException)
+ {
+ thrown = true;
+ }
+
+ Assert.True(thrown);
+ }
+
+ [Fact]
+ public void PopupService_ShowPopup_NullNavigation_ThrowsArgumentNullException()
+ {
+ var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
+ var serviceProvider = services.BuildServiceProvider();
+ var popupService = new PopupService(serviceProvider);
+
+ var thrown = false;
+ try
+ {
+ popupService.ShowPopup(default(INavigation)!);
+ }
+ catch (ArgumentNullException)
+ {
+ thrown = true;
+ }
+
+ Assert.True(thrown);
+ }
+
+ [Fact]
+ public void PopupService_ShowPopup_NullShell_ThrowsArgumentNullException()
+ {
+ var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
+ var serviceProvider = services.BuildServiceProvider();
+ var popupService = new PopupService(serviceProvider);
+
+ var thrown = false;
+ try
+ {
+ popupService.ShowPopup(default(Shell)!);
+ }
+ catch (ArgumentNullException)
+ {
+ thrown = true;
+ }
+
+ Assert.True(thrown);
+ }
+
+ [Fact]
+ public async Task PopupService_ShowPopupAsync_NullPage_ThrowsArgumentNullException()
+ {
+ var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
+ var serviceProvider = services.BuildServiceProvider();
+ var popupService = new PopupService(serviceProvider);
+
+ var thrown = false;
+ try
+ {
+ await popupService.ShowPopupAsync(default(Page)!);
+ }
+ catch (ArgumentNullException)
+ {
+ thrown = true;
+ }
+
+ Assert.True(thrown);
+ }
+
+ [Fact]
+ public async Task PopupService_ShowPopupAsync_NullNavigation_ThrowsArgumentNullException()
+ {
+ var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
+ var serviceProvider = services.BuildServiceProvider();
+ var popupService = new PopupService(serviceProvider);
+
+ var thrown = false;
+ try
+ {
+ await popupService.ShowPopupAsync(default(INavigation)!);
+ }
+ catch (ArgumentNullException)
+ {
+ thrown = true;
+ }
+
+ Assert.True(thrown);
+ }
+
+ [Fact]
+ public async Task PopupService_ShowPopupAsync_NullShell_ThrowsArgumentNullException()
+ {
+ var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
+ var serviceProvider = services.BuildServiceProvider();
+ var popupService = new PopupService(serviceProvider);
+
+ var thrown = false;
+ try
+ {
+ await popupService.ShowPopupAsync(default(Shell)!, null, null, CancellationToken.None);
+ }
+ catch (ArgumentNullException)
+ {
+ thrown = true;
+ }
+
+ Assert.True(thrown);
+ }
+
+ [Fact]
+ public async Task PopupService_ShowPopupAsync_CancellationToken_ThrowsOperationCanceledException()
+ {
+ var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
+ var serviceProvider = services.BuildServiceProvider();
+ var popupService = new PopupService(serviceProvider);
+
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ var navigation = new Page().Navigation;
+ var thrown = false;
+ try
+ {
+ await popupService.ShowPopupAsync(navigation, null, cts.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ thrown = true;
+ }
+
+ Assert.True(thrown);
+ }
+}
+
+#endregion
+
+#region ImageResourceConverter Tests
+
+public class ImageResourceConverterTests
+{
+ [Fact]
+ public void ImageResourceConverter_ConvertFrom_Null_ReturnsNull()
+ {
+ var converter = new ImageResourceConverter();
+ var result = converter.ConvertFrom(null);
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void ImageResourceConverter_DefaultConvertReturnValue_IsNull()
+ {
+ var converter = new ImageResourceConverter();
+ Assert.Null(converter.DefaultConvertReturnValue);
+ }
+
+ [Fact]
+ public void ImageResourceConverter_InheritsFromBaseConverterOneWay()
+ {
+ var converter = new ImageResourceConverter();
+ Assert.IsAssignableFrom>(converter);
+ }
+}
+
+#endregion
+
+#region Options Tests
+
+public class OptionsTests
+{
+ [Fact]
+ public void Options_SetShouldSuppressExceptionsInConverters_Works()
+ {
+ var options = CreateOptions();
+ options.SetShouldSuppressExceptionsInConverters(true);
+
+ var value = GetInternalStaticProperty("CommunityToolkit.Maui.Options", "ShouldSuppressExceptionsInConverters");
+ Assert.True(value);
+
+ options.SetShouldSuppressExceptionsInConverters(false);
+ value = GetInternalStaticProperty("CommunityToolkit.Maui.Options", "ShouldSuppressExceptionsInConverters");
+ Assert.False(value);
+ }
+
+ [Fact]
+ public void Options_SetShouldSuppressExceptionsInAnimations_Works()
+ {
+ var options = CreateOptions();
+ options.SetShouldSuppressExceptionsInAnimations(true);
+
+ var value = GetInternalStaticProperty("CommunityToolkit.Maui.Options", "ShouldSuppressExceptionsInAnimations");
+ Assert.True(value);
+
+ options.SetShouldSuppressExceptionsInAnimations(false);
+ value = GetInternalStaticProperty("CommunityToolkit.Maui.Options", "ShouldSuppressExceptionsInAnimations");
+ Assert.False(value);
+ }
+
+ [Fact]
+ public void Options_SetShouldSuppressExceptionsInBehaviors_Works()
+ {
+ var options = CreateOptions();
+ options.SetShouldSuppressExceptionsInBehaviors(true);
+
+ var value = GetInternalStaticProperty("CommunityToolkit.Maui.Options", "ShouldSuppressExceptionsInBehaviors");
+ Assert.True(value);
+
+ options.SetShouldSuppressExceptionsInBehaviors(false);
+ value = GetInternalStaticProperty("CommunityToolkit.Maui.Options", "ShouldSuppressExceptionsInBehaviors");
+ Assert.False(value);
+ }
+
+ static Options CreateOptions()
+ {
+ var type = typeof(Options);
+ var constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
+ Assert.NotNull(constructor);
+ return (Options)constructor.Invoke(null);
+ }
+
+ static T GetInternalStaticProperty(string typeName, string propertyName)
+ {
+ var assembly = typeof(Options).Assembly;
+ var type = assembly.GetType(typeName);
+ Assert.NotNull(type);
+
+ var property = type.GetProperty(propertyName, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
+ Assert.NotNull(property);
+
+ var value = property.GetValue(null);
+ Assert.NotNull(value);
+ return (T)value;
+ }
+}
+
+#endregion
+
+#region NavigationBar Tests (Android-specific, test shared API)
+
+public class NavigationBarTests
+{
+ [Fact]
+ public void NavigationBar_ColorProperty_Exists()
+ {
+ var type = typeof(CommunityToolkit.Maui.PlatformConfiguration.AndroidSpecific.NavigationBar);
+ var property = type.GetField("ColorProperty", BindingFlags.Public | BindingFlags.Static);
+ Assert.NotNull(property);
+ }
+
+ [Fact]
+ public void NavigationBar_StyleProperty_Exists()
+ {
+ var type = typeof(CommunityToolkit.Maui.PlatformConfiguration.AndroidSpecific.NavigationBar);
+ var property = type.GetField("StyleProperty", BindingFlags.Public | BindingFlags.Static);
+ Assert.NotNull(property);
+ }
+
+ [Fact]
+ public void NavigationBar_GetColor_ReturnsDefault()
+ {
+ var page = new Page();
+ var color = CommunityToolkit.Maui.PlatformConfiguration.AndroidSpecific.NavigationBar.GetColor(page);
+ Assert.NotNull(color);
+ }
+
+ [Fact]
+ public void NavigationBar_SetColor_UpdatesValue()
+ {
+ var page = new Page();
+ CommunityToolkit.Maui.PlatformConfiguration.AndroidSpecific.NavigationBar.SetColor(page, Colors.Red);
+ var color = CommunityToolkit.Maui.PlatformConfiguration.AndroidSpecific.NavigationBar.GetColor(page);
+ Assert.Equal(Colors.Red, color);
+ }
+
+ [Fact]
+ public void NavigationBar_GetStyle_ReturnsDefault()
+ {
+ var page = new Page();
+ var style = CommunityToolkit.Maui.PlatformConfiguration.AndroidSpecific.NavigationBar.GetStyle(page);
+ Assert.Equal(NavigationBarStyle.Default, style);
+ }
+
+ [Fact]
+ public void NavigationBar_SetStyle_UpdatesValue()
+ {
+ var page = new Page();
+ CommunityToolkit.Maui.PlatformConfiguration.AndroidSpecific.NavigationBar.SetStyle(page, NavigationBarStyle.LightContent);
+ var style = CommunityToolkit.Maui.PlatformConfiguration.AndroidSpecific.NavigationBar.GetStyle(page);
+ Assert.Equal(NavigationBarStyle.LightContent, style);
+ }
+}
+
+#endregion
+
+#region BasePlatformBehavior Tests
+
+public class BasePlatformBehaviorTests
+{
+ [Fact]
+ public void TouchBehavior_IsBasePlatformBehavior()
+ {
+ var behavior = new TouchBehavior();
+ Assert.IsAssignableFrom>(behavior);
+ }
+
+ [Fact]
+ public void IconTintColorBehavior_IsBasePlatformBehavior()
+ {
+ var behavior = new IconTintColorBehavior();
+ Assert.IsAssignableFrom>(behavior);
+ }
+}
+
+#endregion
+
+#region PopupService Registration Tests
+
+public class PopupServiceRegistrationTests
+{
+ [Fact]
+ public void PopupService_CanBeResolvedFromServiceProvider()
+ {
+ var services = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
+ services.AddSingleton();
+ var serviceProvider = services.BuildServiceProvider();
+
+ var popupService = serviceProvider.GetService();
+ Assert.NotNull(popupService);
+ Assert.IsType(popupService);
+ }
+}
+
+#endregion
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Tests/Behaviors/BehaviorsTests.cs b/src/CommunityToolkit.Maui.DeviceTests/Tests/Behaviors/BehaviorsTests.cs
new file mode 100644
index 0000000000..288595bb71
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Tests/Behaviors/BehaviorsTests.cs
@@ -0,0 +1,762 @@
+using System.Text.RegularExpressions;
+using CommunityToolkit.Maui.Behaviors;
+using Xunit;
+
+namespace CommunityToolkit.Maui.DeviceTests.Tests.Behaviors;
+
+public class TextValidationBehaviorTests
+{
+ [Fact]
+ public async Task ForceValidate_ValidLength_ReturnsValid()
+ {
+ var behavior = new TextValidationBehavior
+ {
+ MinimumLength = 3,
+ MaximumLength = 10,
+ RegexPattern = ".*",
+ Value = "hello"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_TooShort_ReturnsInvalid()
+ {
+ var behavior = new TextValidationBehavior
+ {
+ MinimumLength = 5,
+ MaximumLength = 10,
+ RegexPattern = ".*",
+ Value = "hi"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_TooLong_ReturnsInvalid()
+ {
+ var behavior = new TextValidationBehavior
+ {
+ MinimumLength = 1,
+ MaximumLength = 3,
+ RegexPattern = ".*",
+ Value = "hello world"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_NullValue_ReturnsInvalid()
+ {
+ var behavior = new TextValidationBehavior
+ {
+ MinimumLength = 0,
+ MaximumLength = 100,
+ RegexPattern = ".*",
+ Value = null
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_RegexMatch_ReturnsValid()
+ {
+ var behavior = new TextValidationBehavior
+ {
+ RegexPattern = "^[a-z]+$",
+ Value = "hello"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_RegexNoMatch_ReturnsInvalid()
+ {
+ var behavior = new TextValidationBehavior
+ {
+ RegexPattern = "^[a-z]+$",
+ Value = "Hello123"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_RegexIgnoreCase_ReturnsValid()
+ {
+ var behavior = new TextValidationBehavior
+ {
+ RegexPattern = "^[a-z]+$",
+ RegexOptions = RegexOptions.IgnoreCase,
+ Value = "Hello"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_TrimDecoration_TrimsBeforeValidation()
+ {
+ var behavior = new TextValidationBehavior
+ {
+ MinimumLength = 3,
+ MaximumLength = 5,
+ RegexPattern = ".*",
+ DecorationFlags = TextDecorationFlags.Trim,
+ Value = " hi "
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_NullToEmptyDecoration_NullValue_IsInvalid()
+ {
+ var behavior = new TextValidationBehavior
+ {
+ MinimumLength = 0,
+ MaximumLength = 100,
+ RegexPattern = ".*",
+ DecorationFlags = TextDecorationFlags.NullToEmpty,
+ Value = null
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ // The NullToEmpty decoration in TextValidationBehavior.Decorate(string?) is not invoked
+ // through the validation pipeline because ValidationBehavior.Decorate(object?)
+ // returns (T?)value directly without delegating to the typed override.
+ // Therefore null fails the "value != null" check in ValidateAsync.
+ Assert.False(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task IsNotValid_IsOppositeOfIsValid()
+ {
+ var behavior = new TextValidationBehavior
+ {
+ RegexPattern = ".*",
+ Value = "test"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(behavior.IsValid);
+ Assert.False(behavior.IsNotValid);
+ }
+}
+
+public class CharactersValidationBehaviorTests
+{
+ [Fact]
+ public async Task ForceValidate_DigitCountInRange_ReturnsValid()
+ {
+ var behavior = new CharactersValidationBehavior
+ {
+ CharacterType = CharacterType.Digit,
+ MinimumCharacterTypeCount = 1,
+ MaximumCharacterTypeCount = 3,
+ RegexPattern = ".*",
+ Value = "abc12"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_TooFewDigits_ReturnsInvalid()
+ {
+ var behavior = new CharactersValidationBehavior
+ {
+ CharacterType = CharacterType.Digit,
+ MinimumCharacterTypeCount = 3,
+ MaximumCharacterTypeCount = 10,
+ RegexPattern = ".*",
+ Value = "abc1"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_TooManyDigits_ReturnsInvalid()
+ {
+ var behavior = new CharactersValidationBehavior
+ {
+ CharacterType = CharacterType.Digit,
+ MinimumCharacterTypeCount = 0,
+ MaximumCharacterTypeCount = 2,
+ RegexPattern = ".*",
+ Value = "a1234"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_UppercaseLetters_CountedCorrectly()
+ {
+ var behavior = new CharactersValidationBehavior
+ {
+ CharacterType = CharacterType.UppercaseLetter,
+ MinimumCharacterTypeCount = 2,
+ MaximumCharacterTypeCount = 5,
+ RegexPattern = ".*",
+ Value = "Hello World"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_NonAlphanumericSymbol_CountedCorrectly()
+ {
+ var behavior = new CharactersValidationBehavior
+ {
+ CharacterType = CharacterType.NonAlphanumericSymbol,
+ MinimumCharacterTypeCount = 1,
+ MaximumCharacterTypeCount = 3,
+ RegexPattern = ".*",
+ Value = "hello!"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_Whitespace_CountedCorrectly()
+ {
+ var behavior = new CharactersValidationBehavior
+ {
+ CharacterType = CharacterType.Whitespace,
+ MinimumCharacterTypeCount = 1,
+ MaximumCharacterTypeCount = 2,
+ RegexPattern = ".*",
+ Value = "a b"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_AnyCharacterType_CountsAll()
+ {
+ var behavior = new CharactersValidationBehavior
+ {
+ CharacterType = CharacterType.Any,
+ MinimumCharacterTypeCount = 5,
+ MaximumCharacterTypeCount = 10,
+ RegexPattern = ".*",
+ Value = "abc12"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(behavior.IsValid);
+ }
+}
+
+public class EmailValidationBehaviorTests
+{
+ [Theory]
+ [InlineData("test@example.com", true)]
+ [InlineData("user.name@domain.org", true)]
+ [InlineData("user+tag@sub.domain.com", true)]
+ [InlineData("invalid", false)]
+ [InlineData("@domain.com", false)]
+ [InlineData("user@", false)]
+ [InlineData("user @domain.com", false)]
+ [InlineData("", false)]
+ public async Task ForceValidate_ValidatesEmailFormat(string email, bool expectedValid)
+ {
+ var behavior = new EmailValidationBehavior
+ {
+ Value = email
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.Equal(expectedValid, behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_NullEmail_ReturnsInvalid()
+ {
+ var behavior = new EmailValidationBehavior
+ {
+ Value = null
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+}
+
+public class NumericValidationBehaviorTests
+{
+ [Theory]
+ [InlineData("42", true)]
+ [InlineData("3.14", true)]
+ [InlineData("-7.5", true)]
+ [InlineData("abc", false)]
+ [InlineData("", false)]
+ public async Task ForceValidate_ValidatesNumericFormat(string input, bool expectedValid)
+ {
+ var behavior = new NumericValidationBehavior
+ {
+ Value = input
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.Equal(expectedValid, behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_ValueInRange_ReturnsValid()
+ {
+ var behavior = new NumericValidationBehavior
+ {
+ MinimumValue = 1,
+ MaximumValue = 100,
+ Value = "50"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_ValueBelowMin_ReturnsInvalid()
+ {
+ var behavior = new NumericValidationBehavior
+ {
+ MinimumValue = 10,
+ MaximumValue = 100,
+ Value = "5"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_ValueAboveMax_ReturnsInvalid()
+ {
+ var behavior = new NumericValidationBehavior
+ {
+ MinimumValue = 1,
+ MaximumValue = 10,
+ Value = "15"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_DecimalPlacesInRange_ReturnsValid()
+ {
+ var behavior = new NumericValidationBehavior
+ {
+ MinimumDecimalPlaces = 1,
+ MaximumDecimalPlaces = 3,
+ Value = "3.14"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_TooManyDecimalPlaces_ReturnsInvalid()
+ {
+ var behavior = new NumericValidationBehavior
+ {
+ MinimumDecimalPlaces = 0,
+ MaximumDecimalPlaces = 2,
+ Value = "3.14159"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_TooFewDecimalPlaces_ReturnsInvalid()
+ {
+ var behavior = new NumericValidationBehavior
+ {
+ MinimumDecimalPlaces = 2,
+ MaximumDecimalPlaces = 5,
+ Value = "3"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_NullValue_ReturnsInvalid()
+ {
+ var behavior = new NumericValidationBehavior
+ {
+ Value = null
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+}
+
+public class RequiredStringValidationBehaviorTests
+{
+ [Fact]
+ public async Task ForceValidate_ExactMatch_MatchingString_ReturnsValid()
+ {
+ var behavior = new RequiredStringValidationBehavior
+ {
+ RequiredString = "password123",
+ ExactMatch = true,
+ Value = "password123"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_ExactMatch_DifferentString_ReturnsInvalid()
+ {
+ var behavior = new RequiredStringValidationBehavior
+ {
+ RequiredString = "password123",
+ ExactMatch = true,
+ Value = "password12"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_ContainsMatch_SubstringPresent_ReturnsValid()
+ {
+ var behavior = new RequiredStringValidationBehavior
+ {
+ RequiredString = "world",
+ ExactMatch = false,
+ Value = "hello world"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_ContainsMatch_SubstringAbsent_ReturnsInvalid()
+ {
+ var behavior = new RequiredStringValidationBehavior
+ {
+ RequiredString = "xyz",
+ ExactMatch = false,
+ Value = "hello world"
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_NullValue_ReturnsInvalid()
+ {
+ var behavior = new RequiredStringValidationBehavior
+ {
+ RequiredString = "test",
+ Value = null
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+}
+
+public class UriValidationBehaviorTests
+{
+ [Theory]
+ [InlineData("https://example.com", true)]
+ [InlineData("http://example.com/path?q=1", true)]
+ [InlineData("ftp://files.example.com", true)]
+ [InlineData("not a uri", false)]
+ [InlineData("", false)]
+ public async Task ForceValidate_ValidatesAbsoluteUri(string uri, bool expectedValid)
+ {
+ var behavior = new UriValidationBehavior
+ {
+ UriKind = UriKind.Absolute,
+ Value = uri
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.Equal(expectedValid, behavior.IsValid);
+ }
+
+ [Theory]
+ [InlineData("/relative/path", true)]
+ [InlineData("relative/path", true)]
+ [InlineData("https://example.com", true)]
+ public async Task ForceValidate_RelativeOrAbsolute_AcceptsBoth(string uri, bool expectedValid)
+ {
+ var behavior = new UriValidationBehavior
+ {
+ UriKind = UriKind.RelativeOrAbsolute,
+ Value = uri
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.Equal(expectedValid, behavior.IsValid);
+ }
+
+ [Theory]
+ [InlineData("/relative/path", true)]
+ [InlineData("relative/path", true)]
+ [InlineData("https://example.com", false)]
+ public async Task ForceValidate_RelativeOnly_RejectsAbsolute(string uri, bool expectedValid)
+ {
+ var behavior = new UriValidationBehavior
+ {
+ UriKind = UriKind.Relative,
+ Value = uri
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.Equal(expectedValid, behavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_NullValue_ReturnsInvalid()
+ {
+ var behavior = new UriValidationBehavior
+ {
+ Value = null
+ };
+
+ await behavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(behavior.IsValid);
+ }
+}
+
+public class MultiValidationBehaviorTests
+{
+ [Fact]
+ public async Task ForceValidate_AllChildrenValid_ReturnsValid()
+ {
+ var multiBehavior = new MultiValidationBehavior();
+
+ var child1 = new RequiredStringValidationBehavior
+ {
+ RequiredString = "hello",
+ ExactMatch = false
+ };
+ MultiValidationBehavior.SetError(child1, "Must contain hello");
+
+ var child2 = new TextValidationBehavior
+ {
+ MinimumLength = 1,
+ MaximumLength = 100,
+ RegexPattern = ".*"
+ };
+ MultiValidationBehavior.SetError(child2, "Length must be 1-100");
+
+ multiBehavior.Children.Add(child1);
+ multiBehavior.Children.Add(child2);
+ multiBehavior.Value = "hello world";
+
+ await multiBehavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(multiBehavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_OneChildInvalid_ReturnsInvalid()
+ {
+ var multiBehavior = new MultiValidationBehavior();
+
+ var child1 = new RequiredStringValidationBehavior
+ {
+ RequiredString = "hello",
+ ExactMatch = false
+ };
+ MultiValidationBehavior.SetError(child1, "Must contain hello");
+
+ var child2 = new TextValidationBehavior
+ {
+ MinimumLength = 1,
+ MaximumLength = 5,
+ RegexPattern = ".*"
+ };
+ MultiValidationBehavior.SetError(child2, "Length must be 1-5");
+
+ multiBehavior.Children.Add(child1);
+ multiBehavior.Children.Add(child2);
+ multiBehavior.Value = "hello world this is too long";
+
+ await multiBehavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(multiBehavior.IsValid);
+ }
+
+ [Fact]
+ public async Task ForceValidate_InvalidChild_PopulatesErrors()
+ {
+ var multiBehavior = new MultiValidationBehavior();
+
+ var child = new RequiredStringValidationBehavior
+ {
+ RequiredString = "required",
+ ExactMatch = true
+ };
+ MultiValidationBehavior.SetError(child, "Value is required");
+
+ multiBehavior.Children.Add(child);
+ multiBehavior.Value = "wrong";
+
+ await multiBehavior.ForceValidate(CancellationToken.None);
+
+ Assert.False(multiBehavior.IsValid);
+ Assert.NotNull(multiBehavior.Errors);
+ Assert.Contains("Value is required", multiBehavior.Errors);
+ }
+
+ [Fact]
+ public async Task ForceValidate_NoChildren_ReturnsValid()
+ {
+ var multiBehavior = new MultiValidationBehavior
+ {
+ Value = "anything"
+ };
+
+ await multiBehavior.ForceValidate(CancellationToken.None);
+
+ Assert.True(multiBehavior.IsValid);
+ }
+}
+
+public class ValidationBehaviorFlagsTests
+{
+ [Fact]
+ public void ValidationFlags_HasExpectedValues()
+ {
+ Assert.Equal(0, (int)ValidationFlags.None);
+ Assert.Equal(1, (int)ValidationFlags.ValidateOnAttaching);
+ Assert.Equal(2, (int)ValidationFlags.ValidateOnFocused);
+ Assert.Equal(4, (int)ValidationFlags.ValidateOnUnfocused);
+ Assert.Equal(8, (int)ValidationFlags.ValidateOnValueChanged);
+ Assert.Equal(16, (int)ValidationFlags.ForceMakeValidWhenFocused);
+ }
+
+ [Fact]
+ public void ValidationFlags_CanBeCombined()
+ {
+ var combined = ValidationFlags.ValidateOnAttaching | ValidationFlags.ValidateOnValueChanged;
+
+ Assert.True(combined.HasFlag(ValidationFlags.ValidateOnAttaching));
+ Assert.True(combined.HasFlag(ValidationFlags.ValidateOnValueChanged));
+ Assert.False(combined.HasFlag(ValidationFlags.ValidateOnFocused));
+ }
+}
+
+public class TextDecorationFlagsTests
+{
+ [Fact]
+ public void TextDecorationFlags_HasExpectedValues()
+ {
+ Assert.Equal(0, (int)TextDecorationFlags.None);
+ Assert.Equal(1, (int)TextDecorationFlags.TrimStart);
+ Assert.Equal(2, (int)TextDecorationFlags.TrimEnd);
+ Assert.Equal(3, (int)TextDecorationFlags.Trim);
+ Assert.Equal(4, (int)TextDecorationFlags.NullToEmpty);
+ Assert.Equal(8, (int)TextDecorationFlags.NormalizeWhiteSpace);
+ }
+}
+
+public class CharacterTypeTests
+{
+ [Fact]
+ public void CharacterType_HasExpectedValues()
+ {
+ Assert.Equal(1, (int)CharacterType.LowercaseLetter);
+ Assert.Equal(2, (int)CharacterType.UppercaseLetter);
+ Assert.Equal(3, (int)CharacterType.Letter);
+ Assert.Equal(4, (int)CharacterType.Digit);
+ Assert.Equal(7, (int)CharacterType.Alphanumeric);
+ Assert.Equal(8, (int)CharacterType.Whitespace);
+ Assert.Equal(16, (int)CharacterType.NonAlphanumericSymbol);
+ Assert.Equal(32, (int)CharacterType.LowercaseLatinLetter);
+ Assert.Equal(64, (int)CharacterType.UppercaseLatinLetter);
+ Assert.Equal(96, (int)CharacterType.LatinLetter);
+ Assert.Equal(31, (int)CharacterType.Any);
+ }
+
+ [Fact]
+ public void CharacterType_Letter_IsCombinationOfLowerAndUpper()
+ {
+ Assert.Equal(CharacterType.LowercaseLetter | CharacterType.UppercaseLetter, CharacterType.Letter);
+ }
+
+ [Fact]
+ public void CharacterType_Alphanumeric_IsCombinationOfLetterAndDigit()
+ {
+ Assert.Equal(CharacterType.Letter | CharacterType.Digit, CharacterType.Alphanumeric);
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Tests/Camera/CameraTests.cs b/src/CommunityToolkit.Maui.DeviceTests/Tests/Camera/CameraTests.cs
new file mode 100644
index 0000000000..cff06e1d14
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Tests/Camera/CameraTests.cs
@@ -0,0 +1,258 @@
+using CommunityToolkit.Maui.Core;
+using CommunityToolkit.Maui.Views;
+using Xunit;
+
+namespace CommunityToolkit.Maui.DeviceTests.Tests.Camera;
+
+public class CameraFlashModeEnumTests
+{
+ [Theory]
+ [InlineData(CameraFlashMode.Off, 0)]
+ [InlineData(CameraFlashMode.On, 1)]
+ [InlineData(CameraFlashMode.Auto, 2)]
+ public void CameraFlashMode_HasExpectedValues(CameraFlashMode mode, int expected)
+ {
+ Assert.Equal(expected, (int)mode);
+ }
+}
+
+public class CameraPositionEnumTests
+{
+ [Theory]
+ [InlineData(CameraPosition.Unknown, 0)]
+ [InlineData(CameraPosition.Rear, 1)]
+ [InlineData(CameraPosition.Front, 2)]
+ public void CameraPosition_HasExpectedValues(CameraPosition position, int expected)
+ {
+ Assert.Equal(expected, (int)position);
+ }
+}
+
+public class CameraViewDefaultsTests
+{
+ [Fact]
+ public void CameraViewDefaults_CameraFlashMode_IsOff()
+ {
+ Assert.Equal(CameraFlashMode.Off, CameraViewDefaults.CameraFlashMode);
+ }
+
+ [Fact]
+ public void CameraViewDefaults_IsTorchOn_IsFalse()
+ {
+ Assert.False(CameraViewDefaults.IsTorchOn);
+ }
+
+ [Fact]
+ public void CameraViewDefaults_ZoomFactor_IsOne()
+ {
+ Assert.Equal(1.0f, CameraViewDefaults.ZoomFactor);
+ }
+
+ [Fact]
+ public void CameraViewDefaults_IsAvailable_IsFalse()
+ {
+ Assert.False(CameraViewDefaults.IsAvailable);
+ }
+
+ [Fact]
+ public void CameraViewDefaults_IsCameraBusy_IsFalse()
+ {
+ Assert.False(CameraViewDefaults.IsCameraBusy);
+ }
+
+ [Fact]
+ public void CameraViewDefaults_ImageCaptureResolution_IsZero()
+ {
+ Assert.Equal(Size.Zero, CameraViewDefaults.ImageCaptureResolution);
+ }
+}
+
+public class CameraExceptionTests
+{
+ [Fact]
+ public void CameraException_HasMessage()
+ {
+ var exception = new CameraException("Camera unavailable");
+
+ Assert.Equal("Camera unavailable", exception.Message);
+ }
+}
+
+public class CameraInfoTests
+{
+ ///
+ /// CameraInfo has platform-specific constructor parameters (e.g. MediaFrameSourceGroup on Windows)
+ /// that require real camera hardware to instantiate. Skip these tests in CI/headless environments.
+ ///
+ static CameraInfo CreateCameraInfo(string name, string deviceId, CameraPosition position, bool isFlashSupported, float minZoom, float maxZoom, IEnumerable resolutions)
+ {
+ var cameraInfoType = typeof(CameraInfo);
+ var constructors = cameraInfoType.GetConstructors();
+ Assert.Single(constructors);
+
+ var parameters = constructors[0].GetParameters();
+ var args = new object?[parameters.Length];
+ args[0] = name;
+ args[1] = deviceId;
+ args[2] = position;
+ args[3] = isFlashSupported;
+ args[4] = minZoom;
+ args[5] = maxZoom;
+ args[6] = resolutions;
+
+ // Fill platform-specific parameters with default values
+ for (var i = 7; i < parameters.Length; i++)
+ {
+ args[i] = parameters[i].ParameterType.IsValueType
+ ? Activator.CreateInstance(parameters[i].ParameterType)
+ : null;
+ }
+
+ return (CameraInfo)constructors[0].Invoke(args);
+ }
+
+ [Fact(Skip = "Requires platform-specific camera hardware (MediaFrameSourceGroup on Windows)")]
+ public void CameraInfo_CanBeCreated()
+ {
+ var cameraInfo = CreateCameraInfo(
+ "Test Camera", "device-123", CameraPosition.Rear, true, 1.0f, 5.0f,
+ [new Size(1920, 1080), new Size(1280, 720)]);
+
+ Assert.Equal("Test Camera", cameraInfo.Name);
+ Assert.Equal("device-123", cameraInfo.DeviceId);
+ Assert.Equal(CameraPosition.Rear, cameraInfo.Position);
+ Assert.True(cameraInfo.IsFlashSupported);
+ Assert.Equal(1.0f, cameraInfo.MinimumZoomFactor);
+ Assert.Equal(5.0f, cameraInfo.MaximumZoomFactor);
+ Assert.Equal(2, cameraInfo.SupportedResolutions.Count);
+ }
+
+ [Fact(Skip = "Requires platform-specific camera hardware (MediaFrameSourceGroup on Windows)")]
+ public void CameraInfo_FrontCamera()
+ {
+ var cameraInfo = CreateCameraInfo(
+ "Front Camera", "front-456", CameraPosition.Front, false, 1.0f, 2.0f,
+ [new Size(640, 480)]);
+
+ Assert.Equal(CameraPosition.Front, cameraInfo.Position);
+ Assert.False(cameraInfo.IsFlashSupported);
+ }
+
+ [Fact(Skip = "Requires platform-specific camera hardware (MediaFrameSourceGroup on Windows)")]
+ public void CameraInfo_Equality()
+ {
+ var camera1 = CreateCameraInfo("Cam", "id1", CameraPosition.Rear, true, 1.0f, 5.0f, [new Size(1920, 1080)]);
+ var camera2 = CreateCameraInfo("Cam", "id1", CameraPosition.Rear, true, 1.0f, 5.0f, [new Size(1920, 1080)]);
+
+ Assert.Equal(camera1, camera2);
+ }
+
+ [Fact(Skip = "Requires platform-specific camera hardware (MediaFrameSourceGroup on Windows)")]
+ public void CameraInfo_Inequality()
+ {
+ var camera1 = CreateCameraInfo("Cam1", "id1", CameraPosition.Rear, true, 1.0f, 5.0f, [new Size(1920, 1080)]);
+ var camera2 = CreateCameraInfo("Cam2", "id2", CameraPosition.Front, false, 1.0f, 2.0f, [new Size(640, 480)]);
+
+ Assert.NotEqual(camera1, camera2);
+ }
+}
+
+public class MediaCapturedEventArgsTests
+{
+ [Fact]
+ public void MediaCapturedEventArgs_CarriesStream()
+ {
+ using var stream = new MemoryStream([1, 2, 3]);
+ var args = new MediaCapturedEventArgs(stream);
+
+ Assert.NotNull(args.Media);
+ Assert.Equal(3, args.Media.Length);
+ }
+}
+
+public class MediaCaptureFailedEventArgsTests
+{
+ [Fact]
+ public void MediaCaptureFailedEventArgs_CarriesFailureReason()
+ {
+ var args = new MediaCaptureFailedEventArgs("Camera disconnected");
+
+ Assert.Equal("Camera disconnected", args.FailureReason);
+ }
+}
+
+public class CameraViewBindablePropertyTests
+{
+ [Fact]
+ public void CameraView_DefaultFlashMode()
+ {
+ var cameraView = new CameraView();
+
+ Assert.Equal(CameraFlashMode.Off, cameraView.CameraFlashMode);
+ }
+
+ [Fact]
+ public void CameraView_DefaultIsTorchOn()
+ {
+ var cameraView = new CameraView();
+
+ Assert.False(cameraView.IsTorchOn);
+ }
+
+ [Fact]
+ public void CameraView_DefaultZoomFactor()
+ {
+ var cameraView = new CameraView();
+
+ Assert.Equal(1.0f, cameraView.ZoomFactor);
+ }
+
+ [Fact]
+ public void CameraView_DefaultIsAvailable()
+ {
+ var cameraView = new CameraView();
+
+ Assert.False(cameraView.IsAvailable);
+ }
+
+ [Fact]
+ public void CameraView_DefaultIsBusy()
+ {
+ var cameraView = new CameraView();
+
+ Assert.False(cameraView.IsBusy);
+ }
+
+ [Fact]
+ public void CameraView_CanSetFlashMode()
+ {
+ var cameraView = new CameraView
+ {
+ CameraFlashMode = CameraFlashMode.Auto
+ };
+
+ Assert.Equal(CameraFlashMode.Auto, cameraView.CameraFlashMode);
+ }
+
+ [Fact]
+ public void CameraView_CanSetZoomFactor()
+ {
+ var cameraView = new CameraView
+ {
+ ZoomFactor = 3.5f
+ };
+
+ Assert.Equal(3.5f, cameraView.ZoomFactor);
+ }
+
+ [Fact]
+ public void CameraView_CanSetIsTorchOn()
+ {
+ var cameraView = new CameraView
+ {
+ IsTorchOn = true
+ };
+
+ Assert.True(cameraView.IsTorchOn);
+ }
+}
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Tests/Converters/ConvertersTests.cs b/src/CommunityToolkit.Maui.DeviceTests/Tests/Converters/ConvertersTests.cs
new file mode 100644
index 0000000000..06bdb4eded
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Tests/Converters/ConvertersTests.cs
@@ -0,0 +1,1163 @@
+using System.Collections;
+using System.Globalization;
+using CommunityToolkit.Maui.Converters;
+using Xunit;
+
+namespace CommunityToolkit.Maui.DeviceTests.Tests.Converters;
+
+public class InvertedBoolConverterTests
+{
+ [Theory]
+ [InlineData(true, false)]
+ [InlineData(false, true)]
+ public void ConvertFrom_InvertsValue(bool input, bool expected)
+ {
+ var converter = new InvertedBoolConverter();
+
+ var result = converter.ConvertFrom(input);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData(true, false)]
+ [InlineData(false, true)]
+ public void ConvertBackTo_InvertsValue(bool input, bool expected)
+ {
+ var converter = new InvertedBoolConverter();
+
+ var result = converter.ConvertBackTo(input);
+
+ Assert.Equal(expected, result);
+ }
+}
+
+public class IntToBoolConverterTests
+{
+ [Theory]
+ [InlineData(0, false)]
+ [InlineData(1, true)]
+ [InlineData(-1, true)]
+ [InlineData(42, true)]
+ public void ConvertFrom_IntToBool(int input, bool expected)
+ {
+ var converter = new IntToBoolConverter();
+
+ var result = converter.ConvertFrom(input);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData(true, 1)]
+ [InlineData(false, 0)]
+ public void ConvertBackTo_BoolToInt(bool input, int expected)
+ {
+ var converter = new IntToBoolConverter();
+
+ var result = converter.ConvertBackTo(input);
+
+ Assert.Equal(expected, result);
+ }
+}
+
+public class IsNullConverterTests
+{
+ [Fact]
+ public void ConvertFrom_Null_ReturnsTrue()
+ {
+ var converter = new IsNullConverter();
+
+ var result = converter.ConvertFrom(null);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ConvertFrom_NonNull_ReturnsFalse()
+ {
+ var converter = new IsNullConverter();
+
+ var result = converter.ConvertFrom("hello");
+
+ Assert.False(result);
+ }
+}
+
+public class IsNotNullConverterTests
+{
+ [Fact]
+ public void ConvertFrom_Null_ReturnsFalse()
+ {
+ var converter = new IsNotNullConverter();
+
+ var result = converter.ConvertFrom(null);
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ConvertFrom_NonNull_ReturnsTrue()
+ {
+ var converter = new IsNotNullConverter();
+
+ var result = converter.ConvertFrom(42);
+
+ Assert.True(result);
+ }
+}
+
+public class IsEqualConverterTests
+{
+ [Fact]
+ public void ConvertFrom_EqualValues_ReturnsTrue()
+ {
+ var converter = new IsEqualConverter();
+
+ var result = converter.ConvertFrom("test", "test");
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ConvertFrom_DifferentValues_ReturnsFalse()
+ {
+ var converter = new IsEqualConverter();
+
+ var result = converter.ConvertFrom("test", "other");
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ConvertFrom_BothNull_ReturnsTrue()
+ {
+ var converter = new IsEqualConverter();
+
+ var result = converter.ConvertFrom(null, null);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ConvertFrom_OneNull_ReturnsFalse()
+ {
+ var converter = new IsEqualConverter();
+
+ var result = converter.ConvertFrom("test", null);
+
+ Assert.False(result);
+ }
+}
+
+public class IsNotEqualConverterTests
+{
+ [Fact]
+ public void ConvertFrom_EqualValues_ReturnsFalse()
+ {
+ var converter = new IsNotEqualConverter();
+
+ var result = converter.ConvertFrom(5, 5);
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ConvertFrom_DifferentValues_ReturnsTrue()
+ {
+ var converter = new IsNotEqualConverter();
+
+ var result = converter.ConvertFrom(5, 10);
+
+ Assert.True(result);
+ }
+}
+
+public class IsStringNullOrEmptyConverterTests
+{
+ [Theory]
+ [InlineData(null, true)]
+ [InlineData("", true)]
+ [InlineData("hello", false)]
+ [InlineData(" ", false)]
+ public void ConvertFrom_ChecksNullOrEmpty(string? input, bool expected)
+ {
+ var converter = new IsStringNullOrEmptyConverter();
+
+ var result = converter.ConvertFrom(input);
+
+ Assert.Equal(expected, result);
+ }
+}
+
+public class IsStringNotNullOrEmptyConverterTests
+{
+ [Theory]
+ [InlineData(null, false)]
+ [InlineData("", false)]
+ [InlineData("hello", true)]
+ [InlineData(" ", true)]
+ public void ConvertFrom_ChecksNotNullOrEmpty(string? input, bool expected)
+ {
+ var converter = new IsStringNotNullOrEmptyConverter();
+
+ var result = converter.ConvertFrom(input);
+
+ Assert.Equal(expected, result);
+ }
+}
+
+public class IsStringNullOrWhiteSpaceConverterTests
+{
+ [Theory]
+ [InlineData(null, true)]
+ [InlineData("", true)]
+ [InlineData(" ", true)]
+ [InlineData("hello", false)]
+ public void ConvertFrom_ChecksNullOrWhiteSpace(string? input, bool expected)
+ {
+ var converter = new IsStringNullOrWhiteSpaceConverter();
+
+ var result = converter.ConvertFrom(input);
+
+ Assert.Equal(expected, result);
+ }
+}
+
+public class IsStringNotNullOrWhiteSpaceConverterTests
+{
+ [Theory]
+ [InlineData(null, false)]
+ [InlineData("", false)]
+ [InlineData(" ", false)]
+ [InlineData("hello", true)]
+ public void ConvertFrom_ChecksNotNullOrWhiteSpace(string? input, bool expected)
+ {
+ var converter = new IsStringNotNullOrWhiteSpaceConverter();
+
+ var result = converter.ConvertFrom(input);
+
+ Assert.Equal(expected, result);
+ }
+}
+
+public class IsListNullOrEmptyConverterTests
+{
+ [Fact]
+ public void ConvertFrom_Null_ReturnsTrue()
+ {
+ var converter = new IsListNullOrEmptyConverter();
+
+ var result = converter.ConvertFrom(null);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ConvertFrom_EmptyList_ReturnsTrue()
+ {
+ var converter = new IsListNullOrEmptyConverter();
+
+ var result = converter.ConvertFrom(new List());
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ConvertFrom_NonEmptyList_ReturnsFalse()
+ {
+ var converter = new IsListNullOrEmptyConverter();
+
+ var result = converter.ConvertFrom(new List { 1, 2, 3 });
+
+ Assert.False(result);
+ }
+}
+
+public class IsListNotNullOrEmptyConverterTests
+{
+ [Fact]
+ public void ConvertFrom_Null_ReturnsFalse()
+ {
+ var converter = new IsListNotNullOrEmptyConverter();
+
+ var result = converter.ConvertFrom(null);
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ConvertFrom_EmptyList_ReturnsFalse()
+ {
+ var converter = new IsListNotNullOrEmptyConverter();
+
+ var result = converter.ConvertFrom(new List());
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ConvertFrom_NonEmptyList_ReturnsTrue()
+ {
+ var converter = new IsListNotNullOrEmptyConverter();
+
+ var result = converter.ConvertFrom(new List { 1 });
+
+ Assert.True(result);
+ }
+}
+
+public class BoolToObjectConverterTests
+{
+ [Fact]
+ public void ConvertFrom_True_ReturnsTrueObject()
+ {
+ var converter = new BoolToObjectConverter
+ {
+ TrueObject = "Yes",
+ FalseObject = "No"
+ };
+
+ var result = converter.ConvertFrom(true);
+
+ Assert.Equal("Yes", result);
+ }
+
+ [Fact]
+ public void ConvertFrom_False_ReturnsFalseObject()
+ {
+ var converter = new BoolToObjectConverter
+ {
+ TrueObject = "Yes",
+ FalseObject = "No"
+ };
+
+ var result = converter.ConvertFrom(false);
+
+ Assert.Equal("No", result);
+ }
+
+ [Fact]
+ public void ConvertBackTo_TrueObject_ReturnsTrue()
+ {
+ var converter = new BoolToObjectConverter
+ {
+ TrueObject = "Yes",
+ FalseObject = "No"
+ };
+
+ var result = converter.ConvertBackTo("Yes");
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ConvertBackTo_FalseObject_ReturnsFalse()
+ {
+ var converter = new BoolToObjectConverter
+ {
+ TrueObject = "Yes",
+ FalseObject = "No"
+ };
+
+ var result = converter.ConvertBackTo("No");
+
+ Assert.False(result);
+ }
+}
+
+public class DoubleToIntConverterTests
+{
+ [Theory]
+ [InlineData(3.7, 4)]
+ [InlineData(3.2, 3)]
+ [InlineData(0.0, 0)]
+ [InlineData(-2.5, -2)]
+ public void ConvertFrom_DoubleToInt(double input, int expected)
+ {
+ var converter = new DoubleToIntConverter();
+
+ var result = converter.ConvertFrom(input);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData(5, 5.0)]
+ [InlineData(0, 0.0)]
+ [InlineData(-3, -3.0)]
+ public void ConvertBackTo_IntToDouble(int input, double expected)
+ {
+ var converter = new DoubleToIntConverter();
+
+ var result = converter.ConvertBackTo(input);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_WithRatio_MultipliesBeforeRounding()
+ {
+ var converter = new DoubleToIntConverter
+ {
+ Ratio = 2.0
+ };
+
+ var result = converter.ConvertFrom(3.0);
+
+ Assert.Equal(6, result);
+ }
+}
+
+public class DateTimeOffsetConverterTests
+{
+ [Fact]
+ public void ConvertFrom_DateTimeOffsetToDateTime()
+ {
+ var converter = new DateTimeOffsetConverter();
+ var dateTimeOffset = new DateTimeOffset(2024, 6, 15, 10, 30, 0, TimeSpan.Zero);
+
+ var result = converter.ConvertFrom(dateTimeOffset);
+
+ Assert.Equal(new DateTime(2024, 6, 15, 10, 30, 0), result);
+ }
+
+ [Fact]
+ public void ConvertBackTo_DateTimeToDateTimeOffset()
+ {
+ var converter = new DateTimeOffsetConverter();
+ var dateTime = new DateTime(2024, 6, 15, 10, 30, 0);
+
+ var result = converter.ConvertBackTo(dateTime);
+
+ Assert.Equal(dateTime, result.DateTime);
+ }
+}
+
+public class TimeSpanToSecondsConverterTests
+{
+ [Fact]
+ public void ConvertFrom_TimeSpanToSeconds()
+ {
+ var converter = new TimeSpanToSecondsConverter();
+ var timeSpan = TimeSpan.FromMinutes(2.5);
+
+ var result = converter.ConvertFrom(timeSpan);
+
+ Assert.Equal(150.0, result);
+ }
+
+ [Fact]
+ public void ConvertBackTo_SecondsToTimeSpan()
+ {
+ var converter = new TimeSpanToSecondsConverter();
+
+ var result = converter.ConvertBackTo(90.0);
+
+ Assert.Equal(TimeSpan.FromSeconds(90), result);
+ }
+}
+
+public class EnumToBoolConverterTests
+{
+ [Fact]
+ public void ConvertFrom_MatchingEnum_ReturnsTrue()
+ {
+ var converter = new EnumToBoolConverter();
+
+ var result = converter.ConvertFrom(DayOfWeek.Monday, DayOfWeek.Monday);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ConvertFrom_DifferentEnum_ReturnsFalse()
+ {
+ var converter = new EnumToBoolConverter();
+
+ var result = converter.ConvertFrom(DayOfWeek.Monday, DayOfWeek.Tuesday);
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ConvertFrom_FlaggedEnum_WithMultipleFlags_ReturnsTrue()
+ {
+ var converter = new EnumToBoolConverter();
+
+ // DayOfWeek is not a [Flags] enum, so use a real flags enum (StringSplitOptions).
+ // The converter checks referenceEnumValue.HasFlag(valueToCheck) for flags enums.
+ var result = converter.ConvertFrom(
+ StringSplitOptions.RemoveEmptyEntries,
+ StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ Assert.True(result);
+ }
+}
+
+public class EnumToIntConverterTests
+{
+ [Fact]
+ public void ConvertFrom_EnumToInt()
+ {
+ var converter = new EnumToIntConverter();
+
+ var result = converter.ConvertFrom(DayOfWeek.Wednesday, typeof(DayOfWeek));
+
+ Assert.Equal(3, result);
+ }
+
+ [Fact]
+ public void ConvertBackTo_IntToEnum()
+ {
+ var converter = new EnumToIntConverter();
+
+ var result = converter.ConvertBackTo(3, typeof(DayOfWeek));
+
+ Assert.Equal(DayOfWeek.Wednesday, result);
+ }
+}
+
+public class IndexToArrayItemConverterTests
+{
+ [Fact]
+ public void ConvertFrom_ValidIndex_ReturnsItem()
+ {
+ var converter = new IndexToArrayItemConverter();
+ var array = new[] { "a", "b", "c" };
+
+ var result = converter.ConvertFrom(1, array);
+
+ Assert.Equal("b", result);
+ }
+
+ [Fact]
+ public void ConvertFrom_FirstIndex_ReturnsFirstItem()
+ {
+ var converter = new IndexToArrayItemConverter();
+ var array = new[] { 10, 20, 30 };
+
+ var result = converter.ConvertFrom(0, array);
+
+ Assert.Equal(10, result);
+ }
+
+ [Fact]
+ public void ConvertBackTo_FindsIndex()
+ {
+ var converter = new IndexToArrayItemConverter();
+ var array = new[] { "x", "y", "z" };
+
+ var result = converter.ConvertBackTo("y", array);
+
+ Assert.Equal(1, result);
+ }
+}
+
+public class ListToStringConverterTests
+{
+ [Fact]
+ public void ConvertFrom_JoinsWithDefaultSeparator()
+ {
+ var converter = new ListToStringConverter();
+ var list = new List { "a", "b", "c" };
+
+ // The default Separator is string.Empty, so items are concatenated with no delimiter.
+ var result = converter.ConvertFrom(list);
+
+ Assert.Equal("abc", result);
+ }
+
+ [Fact]
+ public void ConvertFrom_JoinsWithCustomSeparator()
+ {
+ var converter = new ListToStringConverter();
+ var list = new List { "a", "b", "c" };
+
+ var result = converter.ConvertFrom(list, " | ");
+
+ Assert.Equal("a | b | c", result);
+ }
+
+ [Fact]
+ public void ConvertFrom_EmptyList_ReturnsEmpty()
+ {
+ var converter = new ListToStringConverter();
+ var list = new List();
+
+ var result = converter.ConvertFrom(list);
+
+ Assert.Equal(string.Empty, result);
+ }
+}
+
+public class StringToListConverterTests
+{
+ [Fact]
+ public void ConvertFrom_SplitsWithDefaultSeparator()
+ {
+ var converter = new StringToListConverter();
+
+ // The default Separator is a single space, so split on spaces.
+ var result = converter.ConvertFrom("a b c").ToList();
+
+ Assert.Equal(new[] { "a", "b", "c" }, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_SplitsWithCustomSeparator()
+ {
+ var converter = new StringToListConverter();
+
+ var result = converter.ConvertFrom("a | b | c", " | ");
+
+ Assert.Equal(new[] { "a", "b", "c" }, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_NullInput_ReturnsEmpty()
+ {
+ var converter = new StringToListConverter();
+
+ var result = converter.ConvertFrom(null);
+
+ Assert.Empty(result);
+ }
+}
+
+public class TextCaseConverterTests
+{
+ [Theory]
+ [InlineData("hello", TextCaseType.Upper, "HELLO")]
+ [InlineData("HELLO", TextCaseType.Lower, "hello")]
+ [InlineData("hello world", TextCaseType.Upper, "HELLO WORLD")]
+ public void ConvertFrom_ChangesCase(string input, TextCaseType caseType, string expected)
+ {
+ var converter = new TextCaseConverter();
+
+ var result = converter.ConvertFrom(input, caseType);
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_NullInput_ReturnsNull()
+ {
+ var converter = new TextCaseConverter();
+
+ var result = converter.ConvertFrom(null, TextCaseType.Upper);
+
+ Assert.Null(result);
+ }
+}
+
+public class CompareConverterTests
+{
+ [Fact]
+ public void ConvertFrom_Greater_ReturnsTrue()
+ {
+ var converter = new CompareConverter
+ {
+ ComparingValue = 5,
+ ComparisonOperator = CompareConverter.OperatorType.Greater
+ };
+
+ var result = converter.ConvertFrom(10);
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_NotGreater_ReturnsFalse()
+ {
+ var converter = new CompareConverter
+ {
+ ComparingValue = 10,
+ ComparisonOperator = CompareConverter.OperatorType.Greater
+ };
+
+ var result = converter.ConvertFrom(5);
+
+ Assert.Equal(false, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_Equal_ReturnsTrue()
+ {
+ var converter = new CompareConverter
+ {
+ ComparingValue = 5,
+ ComparisonOperator = CompareConverter.OperatorType.Equal
+ };
+
+ var result = converter.ConvertFrom(5);
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_Smaller_ReturnsTrue()
+ {
+ var converter = new CompareConverter
+ {
+ ComparingValue = 10,
+ ComparisonOperator = CompareConverter.OperatorType.Smaller
+ };
+
+ var result = converter.ConvertFrom(5);
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_WithTrueFalseObjects_ReturnsObjects()
+ {
+ var converter = new CompareConverter
+ {
+ ComparingValue = 5,
+ ComparisonOperator = CompareConverter.OperatorType.Greater,
+ TrueObject = "Big",
+ FalseObject = "Small"
+ };
+
+ var result = converter.ConvertFrom(10);
+
+ Assert.Equal("Big", result);
+ }
+
+ [Fact]
+ public void ConvertFrom_GreaterOrEqual_EqualValues_ReturnsTrue()
+ {
+ var converter = new CompareConverter
+ {
+ ComparingValue = 5,
+ ComparisonOperator = CompareConverter.OperatorType.GreaterOrEqual
+ };
+
+ var result = converter.ConvertFrom(5);
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_SmallerOrEqual_EqualValues_ReturnsTrue()
+ {
+ var converter = new CompareConverter
+ {
+ ComparingValue = 5,
+ ComparisonOperator = CompareConverter.OperatorType.SmallerOrEqual
+ };
+
+ var result = converter.ConvertFrom(5);
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_NotEqual_DifferentValues_ReturnsTrue()
+ {
+ var converter = new CompareConverter
+ {
+ ComparingValue = 5,
+ ComparisonOperator = CompareConverter.OperatorType.NotEqual
+ };
+
+ var result = converter.ConvertFrom(10);
+
+ Assert.Equal(true, result);
+ }
+}
+
+public class IsInRangeConverterTests
+{
+ [Fact]
+ public void ConvertFrom_ValueInRange_ReturnsTrue()
+ {
+ var converter = new IsInRangeConverter
+ {
+ MinValue = 1,
+ MaxValue = 10
+ };
+
+ var result = converter.ConvertFrom(5, null);
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_ValueOutOfRange_ReturnsFalse()
+ {
+ var converter = new IsInRangeConverter
+ {
+ MinValue = 1,
+ MaxValue = 10
+ };
+
+ var result = converter.ConvertFrom(15, null);
+
+ Assert.Equal(false, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_ValueAtMinBoundary_ReturnsTrue()
+ {
+ var converter = new IsInRangeConverter
+ {
+ MinValue = 1,
+ MaxValue = 10
+ };
+
+ var result = converter.ConvertFrom(1, null);
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_ValueAtMaxBoundary_ReturnsTrue()
+ {
+ var converter = new IsInRangeConverter
+ {
+ MinValue = 1,
+ MaxValue = 10
+ };
+
+ var result = converter.ConvertFrom(10, null);
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_OnlyMinValue_SetAboveMin_ReturnsTrue()
+ {
+ var converter = new IsInRangeConverter
+ {
+ MinValue = 5
+ };
+
+ var result = converter.ConvertFrom(10, null);
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_OnlyMaxValue_SetBelowMax_ReturnsTrue()
+ {
+ var converter = new IsInRangeConverter
+ {
+ MaxValue = 10
+ };
+
+ var result = converter.ConvertFrom(5, null);
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void ConvertFrom_NoMinMax_ThrowsArgumentException()
+ {
+ var converter = new IsInRangeConverter();
+ var thrown = false;
+
+ try
+ {
+ converter.ConvertFrom(5, null);
+ }
+ catch (ArgumentException)
+ {
+ thrown = true;
+ }
+
+ Assert.True(thrown);
+ }
+
+ [Fact]
+ public void ConvertFrom_WithTrueFalseObjects_ReturnsObjects()
+ {
+ var converter = new IsInRangeConverter
+ {
+ MinValue = 1,
+ MaxValue = 10,
+ TrueObject = "In Range",
+ FalseObject = "Out of Range"
+ };
+
+ var result = converter.ConvertFrom(5, null);
+
+ Assert.Equal("In Range", result);
+ }
+}
+
+public class MathExpressionConverterTests
+{
+ [Theory]
+ [InlineData(10.0, "x*2", 20.0)]
+ [InlineData(5.0, "x+3", 8.0)]
+ [InlineData(10.0, "x/2", 5.0)]
+ [InlineData(3.0, "x^2", 9.0)]
+ public void ConvertFrom_EvaluatesExpression(double input, string expression, double expected)
+ {
+ var converter = new MathExpressionConverter();
+
+ var result = converter.ConvertFrom(input, expression);
+
+ Assert.NotNull(result);
+ Assert.Equal(expected, Convert.ToDouble(result));
+ }
+}
+
+public class VariableMultiValueConverterTests
+{
+ [Fact]
+ public void Convert_AllTrue_ReturnsTrue()
+ {
+ var converter = new VariableMultiValueConverter
+ {
+ ConditionType = MultiBindingCondition.All
+ };
+
+ var result = converter.Convert([true, true, true], typeof(bool));
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void Convert_All_OneFalse_ReturnsFalse()
+ {
+ var converter = new VariableMultiValueConverter
+ {
+ ConditionType = MultiBindingCondition.All
+ };
+
+ var result = converter.Convert([true, false, true], typeof(bool));
+
+ Assert.Equal(false, result);
+ }
+
+ [Fact]
+ public void Convert_Any_OneTrue_ReturnsTrue()
+ {
+ var converter = new VariableMultiValueConverter
+ {
+ ConditionType = MultiBindingCondition.Any
+ };
+
+ var result = converter.Convert([false, true, false], typeof(bool));
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void Convert_Any_AllFalse_ReturnsFalse()
+ {
+ var converter = new VariableMultiValueConverter
+ {
+ ConditionType = MultiBindingCondition.Any
+ };
+
+ var result = converter.Convert([false, false, false], typeof(bool));
+
+ Assert.Equal(false, result);
+ }
+
+ [Fact]
+ public void Convert_None_AllFalse_ReturnsTrue()
+ {
+ var converter = new VariableMultiValueConverter
+ {
+ ConditionType = MultiBindingCondition.None
+ };
+
+ var result = converter.Convert([false, false], typeof(bool));
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void Convert_None_OneTrue_ReturnsFalse()
+ {
+ var converter = new VariableMultiValueConverter
+ {
+ ConditionType = MultiBindingCondition.None
+ };
+
+ var result = converter.Convert([false, true], typeof(bool));
+
+ Assert.Equal(false, result);
+ }
+
+ [Fact]
+ public void Convert_ExactCount_MatchesCount_ReturnsTrue()
+ {
+ var converter = new VariableMultiValueConverter
+ {
+ ConditionType = MultiBindingCondition.Exact,
+ Count = 2
+ };
+
+ var result = converter.Convert([true, true, false], typeof(bool));
+
+ Assert.Equal(true, result);
+ }
+
+ [Fact]
+ public void Convert_GreaterThan_MoreTrue_ReturnsTrue()
+ {
+ var converter = new VariableMultiValueConverter
+ {
+ ConditionType = MultiBindingCondition.GreaterThan,
+ Count = 1
+ };
+
+ var result = converter.Convert([true, true, false], typeof(bool));
+
+ Assert.Equal(true, result);
+ }
+}
+
+public class StateToBooleanConverterTests
+{
+ [Fact]
+ public void ConvertFrom_MatchingState_ReturnsTrue()
+ {
+ var converter = new StateToBooleanConverter();
+
+ var result = converter.ConvertFrom(LayoutState.Loading, LayoutState.Loading);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ConvertFrom_DifferentState_ReturnsFalse()
+ {
+ var converter = new StateToBooleanConverter();
+
+ var result = converter.ConvertFrom(LayoutState.Loading, LayoutState.Error);
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ConvertFrom_DefaultParameter_UsesNone()
+ {
+ var converter = new StateToBooleanConverter();
+
+ var result = converter.ConvertFrom(LayoutState.None);
+
+ Assert.True(result);
+ }
+}
+
+public class SelectedItemEventArgsConverterTests
+{
+ [Fact]
+ public void ConvertFrom_ExtractsSelectedItem()
+ {
+ var converter = new SelectedItemEventArgsConverter();
+ var args = new SelectedItemChangedEventArgs("TestItem", 0);
+
+ var result = converter.ConvertFrom(args);
+
+ Assert.Equal("TestItem", result);
+ }
+
+ [Fact]
+ public void ConvertFrom_Null_ReturnsNull()
+ {
+ var converter = new SelectedItemEventArgsConverter();
+
+ var result = converter.ConvertFrom(null);
+
+ Assert.Null(result);
+ }
+}
+
+public class ColorConverterTests
+{
+ [Fact]
+ public void ColorToBlackOrWhiteConverter_BlackInput_ReturnsBlack()
+ {
+ var converter = new ColorToBlackOrWhiteConverter();
+
+ var result = converter.ConvertFrom(Colors.Black);
+
+ Assert.Equal(Colors.Black, result);
+ }
+
+ [Fact]
+ public void ColorToBlackOrWhiteConverter_WhiteInput_ReturnsWhite()
+ {
+ var converter = new ColorToBlackOrWhiteConverter();
+
+ var result = converter.ConvertFrom(Colors.White);
+
+ Assert.Equal(Colors.White, result);
+ }
+
+ [Fact]
+ public void ColorToInverseColorConverter_InvertsColor()
+ {
+ var converter = new ColorToInverseColorConverter();
+
+ var result = converter.ConvertFrom(new Color(0.2f, 0.4f, 0.6f));
+
+ Assert.NotNull(result);
+ Assert.Equal(0.8f, result.Red, 0.01f);
+ Assert.Equal(0.6f, result.Green, 0.01f);
+ Assert.Equal(0.4f, result.Blue, 0.01f);
+ }
+
+ [Fact]
+ public void ColorToGrayScaleColorConverter_ConvertsToGrayScale()
+ {
+ var converter = new ColorToGrayScaleColorConverter();
+
+ var result = converter.ConvertFrom(new Color(1.0f, 0.0f, 0.0f));
+
+ Assert.NotNull(result);
+ Assert.Equal(result.Red, result.Green, 0.01f);
+ Assert.Equal(result.Green, result.Blue, 0.01f);
+ }
+}
+
+public class ColorToComponentConverterTests
+{
+ [Fact]
+ public void ColorToByteRedConverter_ExtractsRed()
+ {
+ var converter = new ColorToByteRedConverter();
+
+ var result = converter.ConvertFrom(new Color(1.0f, 0.0f, 0.0f));
+
+ Assert.Equal((byte)255, result);
+ }
+
+ [Fact]
+ public void ColorToByteGreenConverter_ExtractsGreen()
+ {
+ var converter = new ColorToByteGreenConverter();
+
+ var result = converter.ConvertFrom(new Color(0.0f, 1.0f, 0.0f));
+
+ Assert.Equal((byte)255, result);
+ }
+
+ [Fact]
+ public void ColorToByteBlueConverter_ExtractsBlue()
+ {
+ var converter = new ColorToByteBlueConverter();
+
+ var result = converter.ConvertFrom(new Color(0.0f, 0.0f, 1.0f));
+
+ Assert.Equal((byte)255, result);
+ }
+
+ [Fact]
+ public void ColorToByteAlphaConverter_ExtractsAlpha()
+ {
+ var converter = new ColorToByteAlphaConverter();
+
+ var result = converter.ConvertFrom(new Color(0.0f, 0.0f, 0.0f, 0.5f));
+
+ Assert.Equal((byte)128, result);
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Tests/Core/AdditionalCoreTests.cs b/src/CommunityToolkit.Maui.DeviceTests/Tests/Core/AdditionalCoreTests.cs
new file mode 100644
index 0000000000..1232ddfa3f
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Tests/Core/AdditionalCoreTests.cs
@@ -0,0 +1,329 @@
+using CommunityToolkit.Maui.Core;
+using CommunityToolkit.Maui.Core.Primitives;
+using CommunityToolkit.Maui.Core.Views;
+using CommunityToolkit.Maui.Media;
+using CommunityToolkit.Maui.Storage;
+using Xunit;
+
+namespace CommunityToolkit.Maui.DeviceTests.Tests.Core;
+
+public class DrawingEventArgsTests
+{
+ [Fact]
+ public void DrawingLineStartedEventArgs_CarriesPoint()
+ {
+ var point = new PointF(10.5f, 20.5f);
+ var args = new DrawingLineStartedEventArgs(point);
+
+ Assert.Equal(point, args.Point);
+ }
+
+ [Fact]
+ public void MauiDrawingStartedEventArgs_CarriesPoint()
+ {
+ var point = new PointF(5f, 15f);
+ var args = new MauiDrawingStartedEventArgs(point);
+
+ Assert.Equal(point, args.Point);
+ }
+
+ [Fact]
+ public void MauiOnDrawingEventArgs_CarriesPoint()
+ {
+ var point = new PointF(100f, 200f);
+ var args = new MauiOnDrawingEventArgs(point);
+
+ Assert.Equal(point, args.Point);
+ }
+
+ [Fact]
+ public void PointDrawnEventArgs_CarriesPoint()
+ {
+ var point = new PointF(0f, 0f);
+ var args = new PointDrawnEventArgs(point);
+
+ Assert.Equal(point, args.Point);
+ }
+
+ [Fact]
+ public void DrawingLineCompletedEventArgs_CarriesLine()
+ {
+ var line = new DrawingLine();
+ var args = new DrawingLineCompletedEventArgs(line);
+
+ Assert.Same(line, args.LastDrawingLine);
+ }
+}
+
+public class RatingViewEnumTests
+{
+ [Theory]
+ [InlineData(RatingViewFillOption.Shape, 0)]
+ [InlineData(RatingViewFillOption.Background, 1)]
+ public void RatingViewFillOption_HasExpectedValues(RatingViewFillOption option, int expected)
+ {
+ Assert.Equal(expected, (int)option);
+ }
+
+ [Theory]
+ [InlineData(RatingViewShape.Star, 0)]
+ [InlineData(RatingViewShape.Heart, 1)]
+ [InlineData(RatingViewShape.Circle, 2)]
+ [InlineData(RatingViewShape.Like, 3)]
+ [InlineData(RatingViewShape.Dislike, 4)]
+ [InlineData(RatingViewShape.Custom, 5)]
+ public void RatingViewShape_HasExpectedValues(RatingViewShape shape, int expected)
+ {
+ Assert.Equal(expected, (int)shape);
+ }
+
+ [Fact]
+ public void RatingViewShape_HasSixValues()
+ {
+ Assert.Equal(6, Enum.GetValues().Length);
+ }
+
+ [Fact]
+ public void RatingViewFillOption_HasTwoValues()
+ {
+ Assert.Equal(2, Enum.GetValues().Length);
+ }
+}
+
+public class ToastDurationEnumTests
+{
+ [Theory]
+ [InlineData(ToastDuration.Short, 0)]
+ [InlineData(ToastDuration.Long, 1)]
+ public void ToastDuration_HasExpectedValues(ToastDuration duration, int expected)
+ {
+ Assert.Equal(expected, (int)duration);
+ }
+}
+
+public class MauiDrawingLineTests
+{
+ [Fact]
+ public void MauiDrawingLine_DefaultLineWidth()
+ {
+ var line = new MauiDrawingLine();
+
+ Assert.Equal(5f, line.LineWidth);
+ }
+
+ [Fact]
+ public void MauiDrawingLine_DefaultLineColor()
+ {
+ var line = new MauiDrawingLine();
+
+ Assert.Equal(Colors.Black, line.LineColor);
+ }
+
+ [Fact]
+ public void MauiDrawingLine_DefaultPoints_IsEmpty()
+ {
+ var line = new MauiDrawingLine();
+
+ Assert.NotNull(line.Points);
+ Assert.Empty(line.Points);
+ }
+
+ [Fact]
+ public void MauiDrawingLine_DefaultGranularity()
+ {
+ var line = new MauiDrawingLine();
+
+ Assert.Equal(5, line.Granularity);
+ }
+
+ [Fact]
+ public void MauiDrawingLine_DefaultShouldSmoothPathWhenDrawn()
+ {
+ var line = new MauiDrawingLine();
+
+ Assert.True(line.ShouldSmoothPathWhenDrawn);
+ }
+
+ [Fact]
+ public void MauiDrawingLine_CanSetProperties()
+ {
+ var line = new MauiDrawingLine
+ {
+ LineWidth = 10f,
+ LineColor = Colors.Red,
+ Granularity = 20,
+ ShouldSmoothPathWhenDrawn = false,
+ };
+
+ Assert.Equal(10f, line.LineWidth);
+ Assert.Equal(Colors.Red, line.LineColor);
+ Assert.Equal(20, line.Granularity);
+ Assert.False(line.ShouldSmoothPathWhenDrawn);
+ }
+
+ [Theory]
+ [InlineData(0, 5)]
+ [InlineData(-10, 5)]
+ [InlineData(3, 5)]
+ [InlineData(5, 5)]
+ [InlineData(100, 100)]
+ public void MauiDrawingLine_Granularity_ClampsToMinimum(int input, int expected)
+ {
+ var line = new MauiDrawingLine
+ {
+ Granularity = input
+ };
+
+ Assert.Equal(expected, line.Granularity);
+ }
+
+ [Fact]
+ public void MauiDrawingLine_CanAddPoints()
+ {
+ var line = new MauiDrawingLine();
+ line.Points.Add(new PointF(1, 2));
+ line.Points.Add(new PointF(3, 4));
+
+ Assert.Equal(2, line.Points.Count);
+ }
+}
+
+public class ImageOptionsTests
+{
+ [Fact]
+ public void ImagePointOptions_StoresProperties()
+ {
+ var points = new List { new(0, 0), new(10, 10) };
+ var size = new Size(100, 200);
+ var options = new ImagePointOptions(points, size, 3f, Colors.Blue, null, null);
+
+ Assert.Equal(2, options.Points.Count);
+ Assert.Equal(size, options.DesiredSize);
+ Assert.Equal(3f, options.LineWidth);
+ Assert.Equal(Colors.Blue, options.StrokeColor);
+ Assert.Null(options.Background);
+ Assert.Null(options.CanvasSize);
+ }
+
+ [Fact]
+ public void ImagePointOptions_WithBackground()
+ {
+ var points = new List { new(5, 5) };
+ var background = new SolidColorBrush(Colors.White);
+ var options = new ImagePointOptions(points, new Size(50, 50), 1f, Colors.Black, background, new Size(200, 200));
+
+ Assert.NotNull(options.Background);
+ Assert.Equal(new Size(200, 200), options.CanvasSize);
+ }
+
+ [Fact]
+ public void ImageLineOptions_JustLines()
+ {
+ var lines = new List { new DrawingLine() };
+ var options = ImageLineOptions.JustLines(lines, new Size(100, 100), null);
+
+ Assert.Single(options.Lines);
+ Assert.Equal(new Size(100, 100), options.DesiredSize);
+ Assert.Null(options.Background);
+ Assert.Null(options.CanvasSize);
+ }
+
+ [Fact]
+ public void ImageLineOptions_FullCanvas()
+ {
+ var lines = new List { new DrawingLine(), new DrawingLine() };
+ var canvasSize = new Size(500, 500);
+ var options = ImageLineOptions.FullCanvas(lines, new Size(100, 100), null, canvasSize);
+
+ Assert.Equal(2, options.Lines.Count);
+ Assert.Equal(canvasSize, options.CanvasSize);
+ }
+}
+
+public class EnsureSuccessTests
+{
+ [Fact]
+ public void FolderPickerResult_EnsureSuccess_DoesNotThrow_WhenSuccessful()
+ {
+ var result = new FolderPickerResult(new Folder("/test", "Test"), null);
+
+ result.EnsureSuccess();
+ }
+
+ [Fact]
+ public void FolderPickerResult_EnsureSuccess_Throws_WhenFailed()
+ {
+ var exception = new FolderPickerException("Pick failed");
+ var result = new FolderPickerResult(null, exception);
+
+ FolderPickerException? caught = null;
+ try
+ {
+ result.EnsureSuccess();
+ }
+ catch (FolderPickerException ex)
+ {
+ caught = ex;
+ }
+
+ Assert.NotNull(caught);
+ Assert.Equal("Pick failed", caught.Message);
+ }
+
+ [Fact]
+ public void FileSaverResult_EnsureSuccess_DoesNotThrow_WhenSuccessful()
+ {
+ var result = new FileSaverResult("/path/to/file.txt", null);
+
+ result.EnsureSuccess();
+ }
+
+ [Fact]
+ public void FileSaverResult_EnsureSuccess_Throws_WhenFailed()
+ {
+ var exception = new FileSaveException("Save failed");
+ var result = new FileSaverResult(null, exception);
+
+ FileSaveException? caught = null;
+ try
+ {
+ result.EnsureSuccess();
+ }
+ catch (FileSaveException ex)
+ {
+ caught = ex;
+ }
+
+ Assert.NotNull(caught);
+ Assert.Equal("Save failed", caught.Message);
+ }
+
+ [Fact]
+ public void SpeechToTextResult_EnsureSuccess_DoesNotThrow_WhenSuccessful()
+ {
+ var result = new SpeechToTextResult("Hello world", null);
+
+ result.EnsureSuccess();
+ }
+
+ [Fact]
+ public void SpeechToTextResult_EnsureSuccess_Throws_WhenFailed()
+ {
+ var exception = new InvalidOperationException("Recognition failed");
+ var result = new SpeechToTextResult(null, exception);
+
+ InvalidOperationException? caught = null;
+ try
+ {
+ result.EnsureSuccess();
+ }
+ catch (InvalidOperationException ex)
+ {
+ caught = ex;
+ }
+
+ Assert.NotNull(caught);
+ Assert.Equal("Recognition failed", caught.Message);
+ }
+}
+
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Tests/Core/DefaultsTests.cs b/src/CommunityToolkit.Maui.DeviceTests/Tests/Core/DefaultsTests.cs
new file mode 100644
index 0000000000..985c3a2cc2
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Tests/Core/DefaultsTests.cs
@@ -0,0 +1,565 @@
+using CommunityToolkit.Maui.Core;
+using Xunit;
+
+namespace CommunityToolkit.Maui.DeviceTests.Tests.Core;
+
+public class AlertDefaultsTests
+{
+ [Fact]
+ public void AlertDefaults_FontSize_Is14()
+ {
+ Assert.Equal(14d, AlertDefaults.FontSize);
+ }
+
+ [Fact]
+ public void AlertDefaults_CharacterSpacing_IsZero()
+ {
+ Assert.Equal(0.0d, AlertDefaults.CharacterSpacing);
+ }
+
+ [Fact]
+ public void AlertDefaults_ActionButtonText_IsOK()
+ {
+ Assert.Equal("OK", AlertDefaults.ActionButtonText);
+ }
+
+ [Fact]
+ public void AlertDefaults_TextColor_IsBlack()
+ {
+ Assert.Equal(Colors.Black, AlertDefaults.TextColor);
+ }
+
+ [Fact]
+ public void AlertDefaults_BackgroundColor_IsLightGray()
+ {
+ Assert.Equal(Colors.LightGray, AlertDefaults.BackgroundColor);
+ }
+}
+
+public class AvatarViewDefaultsTests
+{
+ [Fact]
+ public void AvatarViewDefaults_BorderWidth_Is1()
+ {
+ Assert.Equal(1d, AvatarViewDefaults.BorderWidth);
+ }
+
+ [Fact]
+ public void AvatarViewDefaults_HeightRequest_Is48()
+ {
+ Assert.Equal(48d, AvatarViewDefaults.HeightRequest);
+ }
+
+ [Fact]
+ public void AvatarViewDefaults_WidthRequest_Is48()
+ {
+ Assert.Equal(48d, AvatarViewDefaults.WidthRequest);
+ }
+
+ [Fact]
+ public void AvatarViewDefaults_Text_IsQuestionMark()
+ {
+ Assert.Equal("?", AvatarViewDefaults.Text);
+ }
+
+ [Fact]
+ public void AvatarViewDefaults_BorderColor_IsWhite()
+ {
+ Assert.Equal(Colors.White, AvatarViewDefaults.BorderColor);
+ }
+
+ [Fact]
+ public void AvatarViewDefaults_CornerRadius_Is24()
+ {
+ Assert.Equal(new CornerRadius(24, 24, 24, 24), AvatarViewDefaults.CornerRadius);
+ }
+
+ [Fact]
+ public void AvatarViewDefaults_Padding_Is1()
+ {
+ Assert.Equal(new Thickness(1), AvatarViewDefaults.Padding);
+ }
+}
+
+public class DrawingViewDefaultsTests
+{
+ [Fact]
+ public void DrawingViewDefaults_MinimumGranularity_Is5()
+ {
+ Assert.Equal(5, DrawingViewDefaults.MinimumGranularity);
+ }
+
+ [Fact]
+ public void DrawingViewDefaults_LineWidth_Is5()
+ {
+ Assert.Equal(5f, DrawingViewDefaults.LineWidth);
+ }
+
+ [Fact]
+ public void DrawingViewDefaults_ShouldSmoothPathWhenDrawn_IsTrue()
+ {
+ Assert.True(DrawingViewDefaults.ShouldSmoothPathWhenDrawn);
+ }
+
+ [Fact]
+ public void DrawingViewDefaults_IsMultiLineModeEnabled_IsFalse()
+ {
+ Assert.False(DrawingViewDefaults.IsMultiLineModeEnabled);
+ }
+
+ [Fact]
+ public void DrawingViewDefaults_ShouldClearOnFinish_IsFalse()
+ {
+ Assert.False(DrawingViewDefaults.ShouldClearOnFinish);
+ }
+
+ [Fact]
+ public void DrawingViewDefaults_LineColor_IsBlack()
+ {
+ Assert.Equal(Colors.Black, DrawingViewDefaults.LineColor);
+ }
+
+ [Fact]
+ public void DrawingViewDefaults_BackgroundColor_IsLightGray()
+ {
+ Assert.Equal(Colors.LightGray, DrawingViewDefaults.BackgroundColor);
+ }
+}
+
+public class DockLayoutDefaultsTests
+{
+ [Fact]
+ public void DockLayoutDefaults_DockPosition_IsNone()
+ {
+ Assert.Equal(DockPosition.None, DockLayoutDefaults.DockPosition);
+ }
+}
+
+public class ExpanderDefaultsTests
+{
+ [Fact]
+ public void ExpanderDefaults_Direction_IsDown()
+ {
+ Assert.Equal(ExpandDirection.Down, ExpanderDefaults.Direction);
+ }
+}
+
+public class FadeAnimationDefaultsTests
+{
+ [Fact]
+ public void FadeAnimationDefaults_Length_Is300()
+ {
+ Assert.Equal(300u, FadeAnimationDefaults.Length);
+ }
+
+ [Fact]
+ public void FadeAnimationDefaults_Opacity_Is0Point3()
+ {
+ Assert.Equal(0.3, FadeAnimationDefaults.Opacity);
+ }
+}
+
+public class ImageTouchBehaviorDefaultsTests
+{
+ [Fact]
+ public void ImageTouchBehaviorDefaults_DefaultBackgroundImageSource_IsNull()
+ {
+ Assert.Null(ImageTouchBehaviorDefaults.DefaultBackgroundImageSource);
+ }
+
+ [Fact]
+ public void ImageTouchBehaviorDefaults_HoveredBackgroundImageSource_IsNull()
+ {
+ Assert.Null(ImageTouchBehaviorDefaults.HoveredBackgroundImageSource);
+ }
+
+ [Fact]
+ public void ImageTouchBehaviorDefaults_PressedBackgroundImageSource_IsNull()
+ {
+ Assert.Null(ImageTouchBehaviorDefaults.PressedBackgroundImageSource);
+ }
+
+ [Fact]
+ public void ImageTouchBehaviorDefaults_DefaultBackgroundImageAspect_IsAspectFit()
+ {
+ Assert.Equal(Aspect.AspectFit, ImageTouchBehaviorDefaults.DefaultBackgroundImageAspect);
+ }
+
+ [Fact]
+ public void ImageTouchBehaviorDefaults_ShouldSetImageOnAnimationEnd_IsFalse()
+ {
+ Assert.False(ImageTouchBehaviorDefaults.ShouldSetImageOnAnimationEnd);
+ }
+}
+
+public class MaxLengthReachedBehaviorDefaultsTests
+{
+ [Fact]
+ public void MaxLengthReachedBehaviorDefaults_ShouldDismissKeyboardAutomatically_IsFalse()
+ {
+ Assert.False(MaxLengthReachedBehaviorDefaults.ShouldDismissKeyboardAutomatically);
+ }
+
+ [Fact]
+ public void MaxLengthReachedBehaviorDefaults_Command_IsNull()
+ {
+ Assert.Null(MaxLengthReachedBehaviorDefaults.Command);
+ }
+}
+
+public class MultiValidationBehaviorDefaultsTests
+{
+ [Fact]
+ public void MultiValidationBehaviorDefaults_Errors_IsNull()
+ {
+ Assert.Null(MultiValidationBehaviorDefaults.Errors);
+ }
+
+ [Fact]
+ public void MultiValidationBehaviorDefaults_Error_IsNull()
+ {
+ Assert.Null(MultiValidationBehaviorDefaults.Error);
+ }
+}
+
+public class NumericValidationBehaviorDefaultsTests
+{
+ [Fact]
+ public void NumericValidationBehaviorDefaults_MinimumValue_IsNegativeInfinity()
+ {
+ Assert.Equal(double.NegativeInfinity, NumericValidationBehaviorDefaults.MinimumValue);
+ }
+
+ [Fact]
+ public void NumericValidationBehaviorDefaults_MaximumValue_IsPositiveInfinity()
+ {
+ Assert.Equal(double.PositiveInfinity, NumericValidationBehaviorDefaults.MaximumValue);
+ }
+
+ [Fact]
+ public void NumericValidationBehaviorDefaults_MinimumDecimalPlaces_IsZero()
+ {
+ Assert.Equal(0, NumericValidationBehaviorDefaults.MinimumDecimalPlaces);
+ }
+
+ [Fact]
+ public void NumericValidationBehaviorDefaults_MaximumDecimalPlaces_IsIntMaxValue()
+ {
+ Assert.Equal(int.MaxValue, NumericValidationBehaviorDefaults.MaximumDecimalPlaces);
+ }
+}
+
+public class ProgressBarAnimationBehaviorDefaultsTests
+{
+ [Fact]
+ public void ProgressBarAnimationBehaviorDefaults_Progress_IsZero()
+ {
+ Assert.Equal(0.0, ProgressBarAnimationBehaviorDefaults.Progress);
+ }
+
+ [Fact]
+ public void ProgressBarAnimationBehaviorDefaults_Length_Is500()
+ {
+ Assert.Equal(500u, ProgressBarAnimationBehaviorDefaults.Length);
+ }
+
+ [Fact]
+ public void ProgressBarAnimationBehaviorDefaults_Easing_IsLinear()
+ {
+ Assert.Equal(Easing.Linear, ProgressBarAnimationBehaviorDefaults.Easing);
+ }
+}
+
+public class RatingViewDefaultsTests
+{
+ [Fact]
+ public void RatingViewDefaults_Rating_IsZero()
+ {
+ Assert.Equal(0.0, RatingViewDefaults.Rating);
+ }
+
+ [Fact]
+ public void RatingViewDefaults_IsReadOnly_IsFalse()
+ {
+ Assert.False(RatingViewDefaults.IsReadOnly);
+ }
+
+ [Fact]
+ public void RatingViewDefaults_ItemShapeSize_Is20()
+ {
+ Assert.Equal(20.0, RatingViewDefaults.ItemShapeSize);
+ }
+
+ [Fact]
+ public void RatingViewDefaults_MaximumRating_Is5()
+ {
+ Assert.Equal(5, RatingViewDefaults.MaximumRating);
+ }
+
+ [Fact]
+ public void RatingViewDefaults_MaximumRatingLimit_Is10()
+ {
+ Assert.Equal(10, RatingViewDefaults.MaximumRatingLimit);
+ }
+
+ [Fact]
+ public void RatingViewDefaults_ShapeBorderThickness_Is1()
+ {
+ Assert.Equal(1.0, RatingViewDefaults.ShapeBorderThickness);
+ }
+
+ [Fact]
+ public void RatingViewDefaults_Spacing_Is10()
+ {
+ Assert.Equal(10.0, RatingViewDefaults.Spacing);
+ }
+
+ [Fact]
+ public void RatingViewDefaults_Shape_IsStar()
+ {
+ Assert.Equal(RatingViewShape.Star, RatingViewDefaults.Shape);
+ }
+
+ [Fact]
+ public void RatingViewDefaults_FillOption_IsShape()
+ {
+ Assert.Equal(RatingViewFillOption.Shape, RatingViewDefaults.FillOption);
+ }
+
+ [Fact]
+ public void RatingViewDefaults_EmptyShapeColor_IsTransparent()
+ {
+ Assert.Equal(Colors.Transparent, RatingViewDefaults.EmptyShapeColor);
+ }
+
+ [Fact]
+ public void RatingViewDefaults_FillColor_IsYellow()
+ {
+ Assert.Equal(Colors.Yellow, RatingViewDefaults.FillColor);
+ }
+
+ [Fact]
+ public void RatingViewDefaults_ShapeBorderColor_IsGrey()
+ {
+ Assert.Equal(Colors.Grey, RatingViewDefaults.ShapeBorderColor);
+ }
+
+ [Fact]
+ public void RatingViewDefaults_ShapePadding_IsZero()
+ {
+ Assert.Equal(new Thickness(0), RatingViewDefaults.ShapePadding);
+ }
+}
+
+public class RequiredStringValidationBehaviorDefaultsTests
+{
+ [Fact]
+ public void RequiredStringValidationBehaviorDefaults_RequiredString_IsNull()
+ {
+ Assert.Null(RequiredStringValidationBehaviorDefaults.RequiredString);
+ }
+
+ [Fact]
+ public void RequiredStringValidationBehaviorDefaults_ExactMatch_IsTrue()
+ {
+ Assert.True(RequiredStringValidationBehaviorDefaults.ExactMatch);
+ }
+}
+
+public class SpeechToTextOptionsDefaultsTests
+{
+ [Fact]
+ public void SpeechToTextOptionsDefaults_ShouldReportPartialResults_IsTrue()
+ {
+ Assert.True(SpeechToTextOptionsDefaults.ShouldReportPartialResults);
+ }
+
+ [Fact]
+ public void SpeechToTextOptionsDefaults_AutoStopSilenceTimeout_IsTimeSpanMaxValue()
+ {
+ Assert.Equal(TimeSpan.MaxValue, SpeechToTextOptionsDefaults.AutoStopSilenceTimeout);
+ }
+}
+
+public class StateViewDefaultsTests
+{
+ [Fact]
+ public void StateViewDefaults_StateKey_IsEmpty()
+ {
+ Assert.Equal(StateViewDefaults.StateKey, string.Empty);
+ }
+}
+
+public class TouchBehaviorDefaultsTests
+{
+ [Fact]
+ public void TouchBehaviorDefaults_HoveredOpacity_Is1()
+ {
+ Assert.Equal(1d, TouchBehaviorDefaults.HoveredOpacity);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_PressedOpacity_Is1()
+ {
+ Assert.Equal(1d, TouchBehaviorDefaults.PressedOpacity);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_DefaultOpacity_Is1()
+ {
+ Assert.Equal(1d, TouchBehaviorDefaults.DefaultOpacity);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_HoveredScale_Is1()
+ {
+ Assert.Equal(1d, TouchBehaviorDefaults.HoveredScale);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_PressedScale_Is1()
+ {
+ Assert.Equal(1d, TouchBehaviorDefaults.PressedScale);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_DefaultScale_Is1()
+ {
+ Assert.Equal(1d, TouchBehaviorDefaults.DefaultScale);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_LongPressDuration_Is500()
+ {
+ Assert.Equal(500, TouchBehaviorDefaults.LongPressDuration);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_IsEnabled_IsTrue()
+ {
+ Assert.True(TouchBehaviorDefaults.IsEnabled);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_DisallowTouchThreshold_IsZero()
+ {
+ Assert.Equal(0, TouchBehaviorDefaults.DisallowTouchThreshold);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_ShouldMakeChildrenInputTransparent_IsTrue()
+ {
+ Assert.True(TouchBehaviorDefaults.ShouldMakeChildrenInputTransparent);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_CurrentTouchState_IsDefault()
+ {
+ Assert.Equal(TouchState.Default, TouchBehaviorDefaults.CurrentTouchState);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_CurrentTouchStatus_IsCompleted()
+ {
+ Assert.Equal(TouchStatus.Completed, TouchBehaviorDefaults.CurrentTouchStatus);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_CurrentHoverState_IsDefault()
+ {
+ Assert.Equal(HoverState.Default, TouchBehaviorDefaults.CurrentHoverState);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_CurrentHoverStatus_IsExited()
+ {
+ Assert.Equal(HoverStatus.Exited, TouchBehaviorDefaults.CurrentHoverStatus);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_CurrentInteractionStatus_IsCompleted()
+ {
+ Assert.Equal(TouchInteractionStatus.Completed, TouchBehaviorDefaults.CurrentInteractionStatus);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_DefaultBackgroundColor_IsTransparent()
+ {
+ Assert.Equal(Colors.Transparent, TouchBehaviorDefaults.DefaultBackgroundColor);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_HoveredBackgroundColor_IsTransparent()
+ {
+ Assert.Equal(Colors.Transparent, TouchBehaviorDefaults.HoveredBackgroundColor);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_PressedBackgroundColor_IsTransparent()
+ {
+ Assert.Equal(Colors.Transparent, TouchBehaviorDefaults.PressedBackgroundColor);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_DefaultAnimationDuration_IsZero()
+ {
+ Assert.Equal(0, TouchBehaviorDefaults.DefaultAnimationDuration);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_HoveredAnimationDuration_IsZero()
+ {
+ Assert.Equal(0, TouchBehaviorDefaults.HoveredAnimationDuration);
+ }
+
+ [Fact]
+ public void TouchBehaviorDefaults_PressedAnimationDuration_IsZero()
+ {
+ Assert.Equal(0, TouchBehaviorDefaults.PressedAnimationDuration);
+ }
+}
+
+public class UniformItemLayoutDefaultsTests
+{
+ [Fact]
+ public void UniformItemLayoutDefaults_MaxRows_IsIntMaxValue()
+ {
+ Assert.Equal(int.MaxValue, UniformItemLayoutDefaults.MaxRows);
+ }
+
+ [Fact]
+ public void UniformItemLayoutDefaults_MaxColumns_IsIntMaxValue()
+ {
+ Assert.Equal(int.MaxValue, UniformItemLayoutDefaults.MaxColumns);
+ }
+}
+
+public class UriValidationBehaviorDefaultsTests
+{
+ [Fact]
+ public void UriValidationBehaviorDefaults_UriKind_IsRelativeOrAbsolute()
+ {
+ Assert.Equal(UriKind.RelativeOrAbsolute, UriValidationBehaviorDefaults.UriKind);
+ }
+}
+
+public class UserStoppedTypingBehaviorDefaultsTests
+{
+ [Fact]
+ public void UserStoppedTypingBehaviorDefaults_StoppedTypingTimeThreshold_Is1000()
+ {
+ Assert.Equal(1000, UserStoppedTypingBehaviorDefaults.StoppedTypingTimeThreshold);
+ }
+
+ [Fact]
+ public void UserStoppedTypingBehaviorDefaults_MinimumLengthThreshold_IsZero()
+ {
+ Assert.Equal(0, UserStoppedTypingBehaviorDefaults.MinimumLengthThreshold);
+ }
+
+ [Fact]
+ public void UserStoppedTypingBehaviorDefaults_ShouldDismissKeyboardAutomatically_IsFalse()
+ {
+ Assert.False(UserStoppedTypingBehaviorDefaults.ShouldDismissKeyboardAutomatically);
+ }
+}
diff --git a/src/CommunityToolkit.Maui.DeviceTests/Tests/Core/DrawingLineAndMathOperatorTests.cs b/src/CommunityToolkit.Maui.DeviceTests/Tests/Core/DrawingLineAndMathOperatorTests.cs
new file mode 100644
index 0000000000..8cbe27c53f
--- /dev/null
+++ b/src/CommunityToolkit.Maui.DeviceTests/Tests/Core/DrawingLineAndMathOperatorTests.cs
@@ -0,0 +1,240 @@
+using CommunityToolkit.Maui.Core;
+using CommunityToolkit.Maui.Core.Views;
+using Xunit;
+
+namespace CommunityToolkit.Maui.DeviceTests.Tests.Core;
+
+public class DrawingLineTests
+{
+ [Fact]
+ public void DrawingLine_DefaultLineColor_IsBlack()
+ {
+ var line = new DrawingLine();
+
+ Assert.Equal(Colors.Black, line.LineColor);
+ }
+
+ [Fact]
+ public void DrawingLine_DefaultLineWidth_Is5()
+ {
+ var line = new DrawingLine();
+
+ Assert.Equal(5f, line.LineWidth);
+ }
+
+ [Fact]
+ public void DrawingLine_DefaultPoints_IsEmpty()
+ {
+ var line = new DrawingLine();
+
+ Assert.NotNull(line.Points);
+ Assert.Empty(line.Points);
+ }
+
+ [Fact]
+ public void DrawingLine_DefaultGranularity_IsMinimumGranularity()
+ {
+ var line = new DrawingLine();
+
+ Assert.Equal(5, line.Granularity);
+ }
+
+ [Fact]
+ public void DrawingLine_DefaultShouldSmoothPathWhenDrawn_IsTrue()
+ {
+ var line = new DrawingLine();
+
+ Assert.True(line.ShouldSmoothPathWhenDrawn);
+ }
+
+ [Fact]
+ public void DrawingLine_CanSetLineColor()
+ {
+ var line = new DrawingLine
+ {
+ LineColor = Colors.Red
+ };
+
+ Assert.Equal(Colors.Red, line.LineColor);
+ }
+
+ [Fact]
+ public void DrawingLine_CanSetLineWidth()
+ {
+ var line = new DrawingLine
+ {
+ LineWidth = 10f
+ };
+
+ Assert.Equal(10f, line.LineWidth);
+ }
+
+ [Fact]
+ public void DrawingLine_CanAddPoints()
+ {
+ var line = new DrawingLine();
+ line.Points.Add(new PointF(10, 20));
+ line.Points.Add(new PointF(30, 40));
+
+ Assert.Equal(2, line.Points.Count);
+ Assert.Equal(new PointF(10, 20), line.Points[0]);
+ Assert.Equal(new PointF(30, 40), line.Points[1]);
+ }
+
+ [Fact]
+ public void DrawingLine_CanSetShouldSmoothPathWhenDrawn()
+ {
+ var line = new DrawingLine
+ {
+ ShouldSmoothPathWhenDrawn = false
+ };
+
+ Assert.False(line.ShouldSmoothPathWhenDrawn);
+ }
+
+ [Theory]
+ [InlineData(10, 10)]
+ [InlineData(100, 100)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ public void DrawingLine_Granularity_AcceptsValidValues(int input, int expected)
+ {
+ var line = new DrawingLine
+ {
+ Granularity = input
+ };
+
+ Assert.Equal(expected, line.Granularity);
+ }
+
+ [Theory]
+ [InlineData(0, 5)]
+ [InlineData(-1, 5)]
+ [InlineData(1, 5)]
+ [InlineData(4, 5)]
+ public void DrawingLine_Granularity_ClampsToMinimum(int input, int expected)
+ {
+ var line = new DrawingLine
+ {
+ Granularity = input
+ };
+
+ Assert.Equal(expected, line.Granularity);
+ }
+
+ [Fact]
+ public void DrawingLine_ImplementsIDrawingLine()
+ {
+ var line = new DrawingLine();
+
+ Assert.IsAssignableFrom(line);
+ }
+
+ [Fact]
+ public void DrawingLine_CanSetPointsCollection()
+ {
+ var points = new System.Collections.ObjectModel.ObservableCollection
+ {
+ new(0, 0),
+ new(5, 5),
+ new(10, 10),
+ };
+
+ var line = new DrawingLine
+ {
+ Points = points
+ };
+
+ Assert.Equal(3, line.Points.Count);
+ Assert.Same(points, line.Points);
+ }
+}
+
+public class MathOperatorTests
+{
+ [Fact]
+ public void MathOperator_StoresName()
+ {
+ var op = new MathOperator("+", 2, args => Convert.ToDouble(args[0]) + Convert.ToDouble(args[1]));
+
+ Assert.Equal("+", op.Name);
+ }
+
+ [Fact]
+ public void MathOperator_StoresNumericCount()
+ {
+ var op = new MathOperator("sin", 1, args => Math.Sin(Convert.ToDouble(args[0])));
+
+ Assert.Equal(1, op.NumericCount);
+ }
+
+ [Fact]
+ public void MathOperator_StoresCalculateFunc()
+ {
+ Func