Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8695ed8
Add device tests for CommunityToolkit.Maui
ne0rrmatrix Aug 2, 2026
1e5b5d7
Add unit tests for MediaElement and Views components
ne0rrmatrix Aug 2, 2026
e293d0a
Add build mappings for Debug and Release configurations in solution f…
ne0rrmatrix Aug 2, 2026
d3d1950
Potential fix for pull request finding
ne0rrmatrix Aug 2, 2026
58f4a28
Merge branch 'main' into DeviceTests
ne0rrmatrix Aug 2, 2026
c96a70e
Updated to Use Maui XHarness
ne0rrmatrix Aug 2, 2026
830f254
Updated to match Maui Xharness behavior
ne0rrmatrix Aug 2, 2026
eda34bf
Merge branch 'DeviceTests' of https://github.com/ne0rrmatrix/MauiOld …
ne0rrmatrix Aug 2, 2026
4657c7a
Fix bad merge
ne0rrmatrix Aug 2, 2026
2c4af21
Remove unused configuration sections from solution files
ne0rrmatrix Aug 2, 2026
dc33c44
Suppress warning for unused PropertyChanged event in TestPopupViewModel
ne0rrmatrix Aug 2, 2026
8323f87
Enhance DeviceRunner and VisualRunnerPage with timeout handling and d…
ne0rrmatrix Aug 2, 2026
89f409a
Refactor device tests to use DeviceRunners infrastructure
ne0rrmatrix Aug 3, 2026
906b24f
Update .gitignore for benchmarks, test results, CLAUDE.md
ne0rrmatrix Aug 3, 2026
845c5a9
Update test infra: remove test runner, clarify docs, add tools
ne0rrmatrix Aug 3, 2026
b718bd7
Merge branch 'main' into DeviceTests
ne0rrmatrix Aug 3, 2026
cd79712
Remove dotnet-tools.json
ne0rrmatrix Aug 3, 2026
a486ffd
Enhance device test configurations and update Snackbar tests for expe…
ne0rrmatrix Aug 3, 2026
2d6d7e3
Remove unnecessary dependencies for device test jobs in CI workflow
ne0rrmatrix Aug 3, 2026
c3ed00b
Add Xcode version setup step and mark DeviceIdiom test as expected fa…
ne0rrmatrix Aug 3, 2026
203e6df
Add Xcode version environment variable for macOS Catalyst device tests
ne0rrmatrix Aug 3, 2026
13a5515
Refactor tests to eliminate reflection usage for accessing internal m…
ne0rrmatrix Aug 3, 2026
f3adc2b
Remove "ExpectedFailure" trait from various test classes to streamlin…
ne0rrmatrix Aug 4, 2026
3d2b3d8
Add "ExpectedFailure" trait to DeviceIdiom test for improved categori…
ne0rrmatrix Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
298 changes: 298 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand All @@ -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
<TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
```

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<App>()` — DeviceRunners registers its own `VisualRunnerApp` via `UseVisualTestRunner`.

**`.csproj`** — required packages:

```xml
<PackageReference Include="DeviceRunners.VisualRunners.Maui" Version="0.1.0-preview.12" />
<PackageReference Include="DeviceRunners.VisualRunners.Xunit" Version="0.1.0-preview.12" />
<PackageReference Include="DeviceRunners.Testing.Targets" Version="0.1.0-preview.12" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiPackageVersion)" />
<PackageReference Include="xunit" Version="2.9.3" />
```

Also required: `<GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint>` 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<StatusBarBehavior>());
});
}
```

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<IEnumerable<TestAssemblyInfo>> DiscoverTestsAsync(IEnumerable<string> sources);
}

public interface ITestRunner
{
Task<TestRunSummary> RunTestsAsync(IEnumerable<TestCase> testCases);
}

public interface IResultChannelManager
{
Task SendResultsAsync(TestResult result);
}

// Platform abstractions (in DeviceRunners.VisualRunners.Core)
public interface IAppTerminator
{
Task TerminateAsync();
}

public interface IDiagnosticsManager
{
Task<DiagnosticData> 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<App>()` 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.
Expand Down
69 changes: 69 additions & 0 deletions .github/instructions/device-tests.instructions.md
Original file line number Diff line number Diff line change
@@ -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<StatusBarBehavior>());
});
}
```

### 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.
Loading
Loading