You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This branch introduces a comprehensive device testing infrastructure for the .NET MAUI Community Toolkit, powered by DeviceRunners — the same test-runner infrastructure recommended by the .NET MAUI team. Unlike standard xunit unit tests that run in a headless .NET process, device tests run inside a real MAUI application on actual devices and emulators (iOS, Android, Mac Catalyst, Windows). This enables testing of platform-specific behavior — handler creation, layout measurement, control rendering, and platform service interactions — that cannot be verified through unit tests alone.
Update (2026-08-03): All reflection-based tests that accessed internal types have been rewritten to use direct type references via InternalsVisibleTo. This eliminates TypeInitializationException failures on macOS Catalyst where Assembly.GetType() cannot resolve internal types. See Reflection Removal below for details.
Update (2026-10-01): Addressed the Copilot review feedback, and changed how the device tests are invoked. After merging main, global.json opts dotnet test into Microsoft.Testing.Platform, which rejects this VSTest-based project, so CI and local runs now call the DeviceRunners VSTest target through dotnet build -t:VSTest. See CI Pipeline and Running Tests.
MSBuild targets (a replacement VSTest target) that build, deploy, run and produce TRX for device projects
xunit v2.9.3
NuGet
xUnit v2 testing framework
MauiProgram.cs
Static class
Configures UseVisualTestRunner
GlobalUsings.cs
Assembly config
CollectionBehavior (sequential, no parallelization)
CI Pipeline
Device tests run in the dotnet-build.yml workflow via two dedicated jobs.
Both jobs invoke the DeviceRunners VSTest target with dotnet build -t:VSTest rather than dotnet test. global.json sets test.runner to Microsoft.Testing.Platform, and in that mode dotnet test refuses any project that is not an MTP application (The following test projects are using VSTest test runner). DeviceRunners is VSTest-based, so the target is called directly; the filter, logger and results directory that dotnet test would pass are supplied as the VSTestTestCaseFilter, VSTestLogger and VSTestResultsDirectory MSBuild properties.
device_test_windows (windows-latest)
device_test_windows:
name: Run Device Tests (Windows)runs-on: windows-lateststeps:
- checkout
- setup-dotnet (TOOLKIT_NET_VERSION)
- dotnet workload install maui
- Enable Windows Developer Mode (registry)
- dotnet restore
- Install Windows App Runtime framework (MSIX from NuGet cache)
- dotnet build -t:VSTest-f net10.0-windows10.0.19041.0-c Release-p:IsCI=true-p:VSTestTestCaseFilter=Category!=ExpectedFailure-p:VSTestLogger=trx%3BLogFileName=test-results.trx-p:VSTestResultsDirectory=<workspace>/artifacts/test-results
- Upload test results artifact
Key details:
Enables Developer Mode via registry before running (required for sideloaded MAUI apps)
Installs Microsoft.WindowsAppSDK MSIX runtime from the NuGet package cache ($env:USERPROFILE/.nuget/packages)
Filters out ExpectedFailure tests so known-flaky tests don't block CI
Produces TRX test results and MSBuild binlog as artifacts
Uses -r maccatalyst-x64 to target the x64 Mac Catalyst runtime
Selects Xcode via maxim-lobanov/setup-xcode using the job-level CommunityToolkitLibrary_Xcode_Version variable (26.6)
Same Category!=ExpectedFailure filter and TRX output as Windows
Category!=ExpectedFailure filter
Tests marked with [Trait("Category", "ExpectedFailure")] are excluded from CI runs but still execute locally. This prevents known platform-specific issues from blocking PRs while keeping the tests visible for investigation. Use [Trait("Category", "ExpectedFailure")] instead of [Fact(Skip = "...")] — Skip hides the test entirely and may cause silent regressions.
Reflection Removal
Problem
The original device tests used Assembly.GetType() and System.Reflection to access internal types (e.g., AlertDefaults, CameraViewDefaults, MediaElementDefaults, Bounds, WeakReferenceExtensions). On macOS Catalyst, Assembly.GetType() fails to resolve internal types, causing TypeInitializationException in the static reflection helper classes and 128 test failures.
Solution
Two-part fix:
InternalsVisibleTo — Added <InternalsVisibleTo Include="CommunityToolkit.Maui.DeviceTests" /> to 5 projects so their internal types are accessible from device tests:
Bounds, Center, EventIdentifier, EventMessage, InfoWindow, SerializerContext
Test rewrites — 7 test files rewritten to use direct type references instead of reflection:
File
Changes
Tests/Core/DefaultsTests.cs
Removed DefaultsReflectionHelper class (~25 lines). All ~68 tests now use direct access (e.g., AlertDefaults.FontSize, RatingViewDefaults.FillColor).
Tests/Camera/CameraTests.cs
CameraViewDefaultsTests uses CameraViewDefaults.CameraFlashMode, .IsTorchOn, etc. directly. Removed using System.Reflection.
Tests/MediaElement/MediaElementTests.cs
Three classes rewritten: MediaElementDefaultsTests → MediaElementDefaults.Speed, etc.; MediaElementOptionsTests → new MediaElementOptions() + direct property access; StreamExtensionsTests → stream.GetMimeType(). Added using CommunityToolkit.Maui.Core.Extensions.
Tests/Maps/MapsTests.cs
Removed MapsReflectionHelper (~35 lines). All 6 test classes use new Bounds(), new Center(), typeof(EventIdentifier), SerializerContext.Default.EventMessage, etc. Added using CommunityToolkit.Maui.Maps.Handlers.
OptionsTests → new Options() and Options.ShouldSuppressExceptionsIn* directly. Removed the reflective CreateOptions / GetInternalStaticProperty helpers.
xUnit Analyzer Fixes
During the rewrite, several xUnit analyzer violations were also fixed:
xUnit2004: Assert.Equal(true/false, ...) → Assert.True(...) / Assert.False(...) wherever the value is statically a bool. Converter tests whose result is object? keep Assert.Equal(true, result); the analyzer does not flag those, and the assertion also checks the boxed type.
xUnit2000: Swapped expected/actual arguments so constants are the expected argument. Assert.Equal(StateViewDefaults.StateKey, string.Empty) is intentionally in this order: StateKey is a const and string.Empty is not.
xUnit1031: Replaced tcs.Task.Result with await tcs.Task
xUnit1004: No longer suppressed in the project; the four CameraInfo tests use the ExpectedFailure trait instead of Skip.
xUnit1051 (TestContext.Current.CancellationToken) does not apply: the project is on xunit v2, where TestContext does not exist.
Architecture
flowchart TB
subgraph Boot["MAUI App Boot"]
MP["MauiProgram.cs"]
VTR["UseVisualTestRunner(conf => conf
.AddCliConfiguration()
.AddConsoleResultChannel()
.AddTestAssembly(...)
.AddXunit())"]
MP --> VTR
end
subgraph Runner["DeviceRunners Visual Runner"]
D1["1. Discover tests via DeviceRunners.VisualRunners.Xunit"]
D2["2. Execute tests on-device"]
D3["3. Display pass/fail UI with per-test details"]
D4["4. Stream results via TCP (VSTest target) or console"]
D1 --> D2 --> D3 --> D4
end
subgraph CI["dotnet build -t:VSTest (CI)"]
C1["1. Build"]
C2["2. Deploy to device"]
C3["3. Launch app"]
C4["4. App auto-starts, connects via TCP port 16384"]
C5["5. NDJSON events streamed back to CLI"]
C6["6. TRX file generated"]
C1 --> C2 --> C3 --> C4 --> C5 --> C6
end
Boot --> Runner
Runner --> CI
Loading
Test Categories Implemented
Smoke Tests — Verifies the MAUI application boots correctly (app, window, dispatcher, services all available)
Platform Detection — Confirms DeviceInfo.Platform, DeviceInfo.Idiom, and OS version reporting using #if ANDROID / #elif IOS / #elif MACCATALYST / #elif WINDOWS conditional compilation
Handler Tests — Validates that MAUI controls (Label, Button, Entry, StackLayout) create platform handlers via element.ToHandler(context) on the main thread
Camera / Maps / MediaElement — Package-specific integration tests including handler types, serialization, and options configuration
Design Decisions & Conventions
DeviceRunners, not custom runners — Uses the community-standard DeviceRunners packages. Do not build custom XunitFrontController wrappers or DeviceRunner classes — DeviceRunners handles discovery, execution, visual runner UI, result collection, and the headless VSTest target.
Handler creation via element.ToHandler(context) — Uses Application.Current?.Handler?.MauiContext to create handlers directly without replacing window.Page. Must run on MainThread.InvokeOnMainThreadAsync.
No builder.UseMauiApp<App>() — DeviceRunners registers its own VisualRunnerApp via UseVisualTestRunner
All packages from nuget.org — No custom feeds; XHarness is a transitive dependency
dotnet build -t:VSTest, not dotnet test — The root global.json sets test.runner to Microsoft.Testing.Platform for the unit test projects. The DeviceTests project is xunit v2/VSTest (DeviceRunners.Testing.Targets sets <IsTestingPlatformApplication>false</IsTestingPlatformApplication>), so dotnet test rejects it in that mode. Invoking the VSTest target directly lets both coexist without changing global.json.
InternalsVisibleTo for internal type access — Device tests reference internal types via <InternalsVisibleTo> instead of Assembly.GetType() reflection. This avoids TypeInitializationException on macOS Catalyst and makes tests faster and more maintainable.
ExpectedFailure trait for known issues — Use [Trait("Category", "ExpectedFailure")] instead of [Fact(Skip = "...")]. CI filters with Category!=ExpectedFailure so known failures don't block PRs, but tests still run locally for investigation.
How to Use and Integrate Device Testing
Prerequisites
.NET 10 SDK with MAUI workload: dotnet workload install maui
DeviceRunners.Testing.Targets handles build, deploy, run, and TRX automatically. No extra tooling needed.
dotnet test does not work for this project while global.json selects Microsoft.Testing.Platform; it stops with The following test projects are using VSTest test runner.
✅ 877 passed, 0 failed (local run, Category!=ExpectedFailure). The 11 ExpectedFailure tests are excluded; 10 of them fail on Windows.
Android
net10.0-android
Builds with no new warnings. Previously reported passing; tests not re-run after the October changes.
iOS Simulator
net10.0-ios
Previously reported passing; not re-built or re-run after the October changes.
Mac Catalyst
net10.0-maccatalyst
❌ CI job currently fails before the tests run: the runner installs the MacCatalyst 27.0 SDK, which requires Xcode 27, while the job selects Xcode 26.6. The same mismatch breaks the macOS sample app build and is not caused by the test code. Tests last passed in CI in August.
- Created a new project for device tests with necessary configurations.
- Implemented an in-app test runner to execute xUnit tests within the MAUI application.
- Added various test classes to verify platform-specific behaviors, including handler creation and platform detection.
- Configured the application to display a message while tests are running.
- Included platform-specific entry points and manifest files for Android, iOS, MacCatalyst, and Windows.
- Added resources for app icons and splash screens.
- Documented the project structure and instructions for running tests locally in the README.
- Implement tests for MediaElementState and AndroidViewType enums.
- Validate MediaElementDefaults and MediaElementOptions properties.
- Create tests for MediaSource and its derived classes.
- Add tests for Popup, AvatarView, DrawingView, Expander, LazyView, RatingView, and SemanticOrderView.
- Implement Toast and Snackbar tests with platform-specific skips.
- Add tests for DefaultPopupSettings and DefaultPopupOptionsSettings.
- Include AccessModifier tests to verify expected values.
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a new on-device testing harness for the .NET MAUI Community Toolkit by adding a MAUI “DeviceTests” app that discovers and runs xUnit tests via reflection, plus a companion unit test project that validates the runner behavior. It also updates solution definitions and test-runner configuration to support the new test projects.
Changes:
Added CommunityToolkit.Maui.DeviceTests MAUI app project (platform entrypoints, resources, smoke/platform/handler tests, plus many toolkit API tests).
Added CommunityToolkit.Maui.DeviceTests.UnitTests project to validate InAppTestRunner behavior (pass/fail/skip/theory/disposal/cancellation).
Updated .slnx files and repo test configuration (global.json, Directory.Build.props) to include/enable the new testing setup.
Reviewed changes
Copilot reviewed 50 out of 53 changed files in this pull request and generated 3 comments.
Show a summary per file
File
Description
src/CommunityToolkit.Maui.slnx
Adds build configuration mappings and includes the new device test projects in the main solution.
samples/CommunityToolkit.Maui.Sample.slnx
Mirrors .slnx configuration/mappings and includes the device test project in the sample solution.
global.json
Enables Microsoft.Testing.Platform mode via the test section.
Directory.Build.props
Updates commentary around MTP enablement; removes obsolete TestingPlatformDotnetTestSupport usage.
Does this leverage the open Device Tests PR? Does it include its logic/tests? Or does it replace it?
This PR is an independent implementation that replaces #1444 rather than building on it. It does not include any code or tests from that PR.
The key difference is the approach: #1444 relies on the DeviceRunners/XHarness infrastructure (external packages from private NuGet feeds, UseXHarnessTestRunner/UseVisualTestRunner in MauiProgram, and Microsoft.DotNet.XHarness.CLI for CI). This PR instead uses a custom InAppTestRunner — a lightweight, reflection-based test discovery and execution engine that runs xunit tests directly inside the MAUI app with no external runner dependencies or private feeds.
It also adds a companion CommunityToolkit.Maui.DeviceTests.UnitTests project that validates the runner itself (pass/fail/skip/theory/disposal/cancellation scenarios), and targets net10.0 with xunit.v3 + Microsoft.Testing.Platform.
If we prefer the DeviceRunners/XHarness approach from #1444, this PR could be adapted to use that infrastructure instead. Happy to align with whichever direction the team prefers.
I would recommend following the same approach that the .NET MAUI team uses for their Device Tests. That will be the most-robust and future-proof way of proceeding.
I would recommend following the same approach that the .NET MAUI team uses for their Device Tests. That will be the most-robust and future-proof way of proceeding.
- Updated device tests to utilize DeviceRunners for test discovery and execution, replacing the previous XHarness/XunitFrontController approach.
- Removed obsolete App, HeadlessRunnerOptions, and related runner classes.
- Simplified MauiProgram configuration to integrate DeviceRunners visual test runner.
- Updated README and instructions to reflect changes in test execution and runner architecture.
- Removed custom NuGet.config as DeviceRunners packages are now sourced from NuGet.org.
Added BenchmarkDotNet.Artifacts/, device test results, and CLAUDE.md to .gitignore to prevent tracking of benchmark outputs, test artifacts, and the CLAUDE.md file. This keeps the repository clean from generated files and documentation not meant for version control.
Removed "test" runner from global.json. Updated DeviceTests README to clarify DeviceRunners usage and NuGet package sourcing—no custom runners or feeds needed. Added dotnet-tools.json for XHarness CLI, Android, and Apple device tools.
The reason will be displayed to describe this comment to others. Learn more.
I followed what I saw and only added for windows and macos. I do not know if we have access to emulators for android/ios testing? For local testing android/ios works.
…cted failures
- Added expected failure trait for device tests known to fail on certain platforms.
- Updated project settings to use MSIX packaging for Windows.
- Modified Snackbar tests to use the expected failure trait instead of skipping.
This new test project globally suppresses xUnit2032 even though the repository explicitly promotes that diagnostic to an error in Directory.Build.props:225; none of these tests are pre-existing in this project. Update the affected assertions to the analyzer-supported form rather than bypassing the repository-wide analyzer policy.
- copilot-instructions: show the VSTestTestCaseFilter property instead of
the dotnet test --filter option, which this project cannot use
- README: describe the headless VSTest target instead of dotnet test support
- csproj: attribute the entry points to the app, not DeviceRunners
- SnackbarTests: explain that the tests fail on Windows because the app
does not enable Snackbar, regardless of MSIX packaging
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ToRgbaString() formats alpha with the current culture when no culture is supplied. On devices using a decimal-comma locale this returns an alpha such as 0,5, so the invariant 0.5 assertion fails even though the implementation is correct. Pass an explicit invariant culture to keep this device test locale-independent.
Width clamping test uses a child that contributes no width
This does not exercise width clamping: a Top child contributes only to the measured height, so result.Width is zero and the inequality passes regardless of the width constraint. Dock the oversized child left/right and assert the constrained width so a regression in Math.Min(width, widthConstraint) is detectable.
Height clamping test uses a child that contributes no height
This does not exercise height clamping: a Left child contributes only to the measured width, so result.Height is zero and the inequality always passes. Use a top/bottom child whose height exceeds the constraint and assert the constrained height.
Weak-reference test passes without verifying target collection
This conditional lets the test pass without asserting the collected-reference behavior whenever the target survives. Collection also happens while the helper's target local is still in scope, which makes that no-op path more likely. Return the weak reference first, force collection after the no-inline helper has exited, and require the target to be gone before checking GetTargetOrDefault().
…lyst job
- ToRgbaString test passes an invariant culture so it is locale-independent
- DockLayoutManager constraint tests now exercise the clamping they claim to
- Weak-reference test requires the target to be collected instead of passing vacuously
- Mac Catalyst device-test job uses the Xcode 27 runner required by the updated workload
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re: Copilot review. All four "previously missed" findings are valid and fixed in 6d38df2, along with the Catalyst/Xcode mismatch called out in the summary.
Locale-dependent alpha formatting (ExtensionsTests.cs): the test now calls ToRgbaString(CultureInfo.InvariantCulture), so it no longer depends on the device locale.
Width clamping test (LayoutsTests.cs): the oversized child is now docked Left with HorizontalSpacing = 10 and the test asserts Width == 200. The spacing is needed because TestView clamps its own measure to the constraint, so only the trailing spacing pushes the total past it (210) and reaches the Math.Min(width, widthConstraint) clamp.
Height clamping test (LayoutsTests.cs): same change on the other axis, a Top child with VerticalSpacing = 10, asserting Height == 200.
Weak-reference test (InternalExtensionsTests.cs): the conditional is gone. The target is created by a no-inline helper on another thread, collection is forced after that helper has returned (retried up to 10 times), and the test asserts the target is gone before asserting GetTargetOrDefault() returns null.
Catalyst CI job: the job had its own 26.6 Xcode pin, which the Xcode 27 update on main did not touch, and failed with "requires Xcode 27.0". It now runs on the xcode-27 runner with CommunityToolkitSample_Xcode_Version, mirroring the sample build job that already passes with the same workload update.
The affected test classes pass locally on Mac Catalyst (26 passed, 0 failed). The workflow change is verified by the CI run on this commit.
The remaining open thread on MauiProgram.cs (Maps and Windows Snackbar initialization) is unchanged and still waiting on a maintainer decision.
The Xcode 27 Mac Catalyst workload rejects a SupportedOSPlatformVersion below 17.0, matching the sample app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- MauiProgram registers the Maps handlers and enables Snackbar on Windows
- Package.appxmanifest declares the notification activation entries Snackbar requires
- Snackbar tests are no longer tagged ExpectedFailure
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Forcing maccatalyst-x64 on the Xcode 27 runner fails in ILLink with IL2037 errors; the sample build, which uses the default runtime identifier, links cleanly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
I just opened a new Draft PR (I have another big commit coming tomorrow) that builds on top of the work you did in this PR:
Adds fixes to the library where the device tests found a bug
Links Fixed Issues that will close when it’s merged
Adds additional device tests to increase code coverage
You’re welcome to give it a glance when you get a chance. It’s a massive PR, similar to this one. And it is branched off of the branch for this PR.
I recommend we close this PR and move forward with #3344 since it includes the new bug fixes. Once we merge it we can release v16.0.0 since it does add some subtle breaking changes in how executions are thrown/not thrown for certain classes (and in include those breaking changes in the Release Notes).
Yes I agree. I kind of stalled out not sure of exactly where we should go. Appreciate you taking up the mantle and getting it done. I have been looking forward to being able to use device testing for several years and I am happy to see you continuing it. I am closing this PR in favor of the one you started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Labels
None yet
5 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[PR] Add Device Testing Infrastructure (DeviceRunners)
Description of Change
This branch introduces a comprehensive device testing infrastructure for the .NET MAUI Community Toolkit, powered by DeviceRunners — the same test-runner infrastructure recommended by the .NET MAUI team. Unlike standard xunit unit tests that run in a headless .NET process, device tests run inside a real MAUI application on actual devices and emulators (iOS, Android, Mac Catalyst, Windows). This enables testing of platform-specific behavior — handler creation, layout measurement, control rendering, and platform service interactions — that cannot be verified through unit tests alone.
Update (2026-08-03): All reflection-based tests that accessed internal types have been rewritten to use direct type references via
InternalsVisibleTo. This eliminatesTypeInitializationExceptionfailures on macOS Catalyst whereAssembly.GetType()cannot resolve internal types. See Reflection Removal below for details.Update (2026-10-01): Addressed the Copilot review feedback, and changed how the device tests are invoked. After merging
main,global.jsonoptsdotnet testinto Microsoft.Testing.Platform, which rejects this VSTest-based project, so CI and local runs now call the DeviceRunnersVSTesttarget throughdotnet build -t:VSTest. See CI Pipeline and Running Tests.Key Components
CommunityToolkit.Maui.DeviceTestsDeviceRunners.VisualRunners.MauiDeviceRunners.VisualRunners.XunitDeviceRunners.Testing.TargetsVSTesttarget) that build, deploy, run and produce TRX for device projectsxunitv2.9.3MauiProgram.csUseVisualTestRunnerGlobalUsings.csCollectionBehavior(sequential, no parallelization)CI Pipeline
Device tests run in the
dotnet-build.ymlworkflow via two dedicated jobs.Both jobs invoke the DeviceRunners
VSTesttarget withdotnet build -t:VSTestrather thandotnet test.global.jsonsetstest.runnerto Microsoft.Testing.Platform, and in that modedotnet testrefuses any project that is not an MTP application (The following test projects are using VSTest test runner). DeviceRunners is VSTest-based, so the target is called directly; the filter, logger and results directory thatdotnet testwould pass are supplied as theVSTestTestCaseFilter,VSTestLoggerandVSTestResultsDirectoryMSBuild properties.device_test_windows(windows-latest)Key details:
Microsoft.WindowsAppSDKMSIX runtime from the NuGet package cache ($env:USERPROFILE/.nuget/packages)ExpectedFailuretests so known-flaky tests don't block CIdevice_test_maccatalyst(macos-26)Key details:
-r maccatalyst-x64to target the x64 Mac Catalyst runtimemaxim-lobanov/setup-xcodeusing the job-levelCommunityToolkitLibrary_Xcode_Versionvariable (26.6)Category!=ExpectedFailurefilter and TRX output as WindowsCategory!=ExpectedFailurefilterTests marked with
[Trait("Category", "ExpectedFailure")]are excluded from CI runs but still execute locally. This prevents known platform-specific issues from blocking PRs while keeping the tests visible for investigation. Use[Trait("Category", "ExpectedFailure")]instead of[Fact(Skip = "...")]—Skiphides the test entirely and may cause silent regressions.Reflection Removal
Problem
The original device tests used
Assembly.GetType()andSystem.Reflectionto access internal types (e.g.,AlertDefaults,CameraViewDefaults,MediaElementDefaults,Bounds,WeakReferenceExtensions). On macOS Catalyst,Assembly.GetType()fails to resolve internal types, causingTypeInitializationExceptionin the static reflection helper classes and 128 test failures.Solution
Two-part fix:
InternalsVisibleTo— Added<InternalsVisibleTo Include="CommunityToolkit.Maui.DeviceTests" />to 5 projects so their internal types are accessible from device tests:CommunityToolkit.Maui.Core*Defaultsstatic classes (AlertDefaults,AvatarViewDefaults,DrawingViewDefaults, etc.),NullableExtensionsCommunityToolkit.MauiWeakReferenceExtensions,SafeFireAndForgetExtensions,PropertyChangedEventArgsExtensions,CryptographyExtensions,Options(internal ctor + static props)CommunityToolkit.Maui.CameraCameraViewDefaultsCommunityToolkit.Maui.MediaElementMediaElementDefaults,MediaElementOptions(internal ctor + static props),StreamExtensionsCommunityToolkit.Maui.MapsBounds,Center,EventIdentifier,EventMessage,InfoWindow,SerializerContextTests/Core/DefaultsTests.csDefaultsReflectionHelperclass (~25 lines). All ~68 tests now use direct access (e.g.,AlertDefaults.FontSize,RatingViewDefaults.FillColor).Tests/Camera/CameraTests.csCameraViewDefaultsTestsusesCameraViewDefaults.CameraFlashMode,.IsTorchOn, etc. directly. Removedusing System.Reflection.Tests/MediaElement/MediaElementTests.csMediaElementDefaultsTests→MediaElementDefaults.Speed, etc.;MediaElementOptionsTests→new MediaElementOptions()+ direct property access;StreamExtensionsTests→stream.GetMimeType(). Addedusing CommunityToolkit.Maui.Core.Extensions.Tests/Maps/MapsTests.csMapsReflectionHelper(~35 lines). All 6 test classes usenew Bounds(),new Center(),typeof(EventIdentifier),SerializerContext.Default.EventMessage, etc. Addedusing CommunityToolkit.Maui.Maps.Handlers.Tests/Extensions/InternalExtensionsTests.csMauiExtensionsReflectionHelper(~30 lines).CryptographyExtensionsTests→"string".GetMd5Hash().WeakReferenceExtensionsTests→weakRef.GetTargetOrDefault().SafeFireAndForgetExtensionsTests→task.SafeFireAndForget().Tests/Additional/ExtensionsAndEventArgsTests.csPropertyChangedEventArgsExtensionsTests→"propertyName".IsOneOf(...)directly.NullableExtensionsTests→typeof(x).IsNullable()directly.Tests/Additional/PlatformBehaviorsAndServicesTests.csOptionsTests→new Options()andOptions.ShouldSuppressExceptionsIn*directly. Removed the reflectiveCreateOptions/GetInternalStaticPropertyhelpers.xUnit Analyzer Fixes
During the rewrite, several xUnit analyzer violations were also fixed:
Assert.Equal(true/false, ...)→Assert.True(...)/Assert.False(...)wherever the value is statically abool. Converter tests whose result isobject?keepAssert.Equal(true, result); the analyzer does not flag those, and the assertion also checks the boxed type.expectedargument.Assert.Equal(StateViewDefaults.StateKey, string.Empty)is intentionally in this order:StateKeyis aconstandstring.Emptyis not.tcs.Task.Resultwithawait tcs.TaskCameraInfotests use theExpectedFailuretrait instead ofSkip.xUnit1051(TestContext.Current.CancellationToken) does not apply: the project is on xunit v2, whereTestContextdoes not exist.Architecture
flowchart TB subgraph Boot["MAUI App Boot"] MP["MauiProgram.cs"] VTR["UseVisualTestRunner(conf => conf .AddCliConfiguration() .AddConsoleResultChannel() .AddTestAssembly(...) .AddXunit())"] MP --> VTR end subgraph Runner["DeviceRunners Visual Runner"] D1["1. Discover tests via DeviceRunners.VisualRunners.Xunit"] D2["2. Execute tests on-device"] D3["3. Display pass/fail UI with per-test details"] D4["4. Stream results via TCP (VSTest target) or console"] D1 --> D2 --> D3 --> D4 end subgraph CI["dotnet build -t:VSTest (CI)"] C1["1. Build"] C2["2. Deploy to device"] C3["3. Launch app"] C4["4. App auto-starts, connects via TCP port 16384"] C5["5. NDJSON events streamed back to CLI"] C6["6. TRX file generated"] C1 --> C2 --> C3 --> C4 --> C5 --> C6 end Boot --> Runner Runner --> CITest Categories Implemented
DeviceInfo.Platform,DeviceInfo.Idiom, and OS version reporting using#if ANDROID/#elif IOS/#elif MACCATALYST/#elif WINDOWSconditional compilationelement.ToHandler(context)on the main threadTextValidationBehaviortests (valid/invalid lengths, regex patterns, null values)InvertedBoolConverter,IntToBoolConverter,IsNullConverter, color converters, and morePopupandPopupOptionsproperty defaults, event wiring, and configurationDockLayoutManagermeasurement,FolderPickerResult,FileSaverResult,DrawingLine, math operators, primitives, and extension methods*Defaultsstatic classes inCommunityToolkit.Maui.Core(20 classes, ~68 tests for const fields and static properties)CryptographyExtensions.GetMd5Hash,WeakReferenceExtensions.GetTargetOrDefault,SafeFireAndForgetExtensions.SafeFireAndForget,PropertyChangedEventArgsExtensions.IsOneOf)Design Decisions & Conventions
XunitFrontControllerwrappers orDeviceRunnerclasses — DeviceRunners handles discovery, execution, visual runner UI, result collection, and the headlessVSTesttarget.[assembly: CollectionBehavior(DisableTestParallelization = true, MaxParallelThreads = 1)]prevents UI threading issueselement.ToHandler(context)— UsesApplication.Current?.Handler?.MauiContextto create handlers directly without replacingwindow.Page. Must run onMainThread.InvokeOnMainThreadAsync.builder.UseMauiApp<App>()— DeviceRunners registers its ownVisualRunnerAppviaUseVisualTestRunnerGenerateTestingPlatformEntryPoint>false— DeviceRunners providesProgram.Maindotnet build -t:VSTest, notdotnet test— The rootglobal.jsonsetstest.runnerto Microsoft.Testing.Platform for the unit test projects. The DeviceTests project is xunit v2/VSTest (DeviceRunners.Testing.Targetssets<IsTestingPlatformApplication>false</IsTestingPlatformApplication>), sodotnet testrejects it in that mode. Invoking theVSTesttarget directly lets both coexist without changingglobal.json.InternalsVisibleTofor internal type access — Device tests reference internal types via<InternalsVisibleTo>instead ofAssembly.GetType()reflection. This avoidsTypeInitializationExceptionon macOS Catalyst and makes tests faster and more maintainable.ExpectedFailuretrait for known issues — Use[Trait("Category", "ExpectedFailure")]instead of[Fact(Skip = "...")]. CI filters withCategory!=ExpectedFailureso known failures don't block PRs, but tests still run locally for investigation.How to Use and Integrate Device Testing
Prerequisites
dotnet workload install mauiRunning Tests
VSTesttarget (CI / Headless, Recommended)DeviceRunners.Testing.Targetshandles build, deploy, run, and TRX automatically. No extra tooling needed.dotnet testdoes not work for this project whileglobal.jsonselects Microsoft.Testing.Platform; it stops withThe following test projects are using VSTest test runner.Visual Runner (IDE / Interactive)
Or press F5 in VS Code / Visual Studio. The DeviceRunners visual runner displays test results in-app.
Adding New Device Tests
src/CommunityToolkit.Maui.DeviceTests/Tests/<Category>/:MainThread.InvokeOnMainThreadAsync+element.ToHandler(context):InternalsVisibleTois in the target project's.csproj:Then reference the types directly — no reflection needed:
Project Structure
Platforms Tested
Status as of 2026-10-01 (commit 5e98149):
net10.0-windows10.0.19041.0Category!=ExpectedFailure). The 11ExpectedFailuretests are excluded; 10 of them fail on Windows.net10.0-androidnet10.0-iosnet10.0-maccatalyst