Skip to content

Prevent NullReferenceException in LayoutButton - #35284

Merged
kubaflo merged 11 commits into
dotnet:inflight/currentfrom
GamesAgeddon:main
May 2, 2026
Merged

Prevent NullReferenceException in LayoutButton#35284
kubaflo merged 11 commits into
dotnet:inflight/currentfrom
GamesAgeddon:main

Conversation

@GamesAgeddon

Copy link
Copy Markdown
Contributor
  • ICrossPlatformLayout.CrossPlatformArrange can call LayoutButton with a null button so it should be prepared for that.

Fixes #31048

github-actions Bot and others added 11 commits April 29, 2026 07:06
The credential provider plugin and Azure Artifacts feed add
significant overhead locally (~10+ min builds reported by team).
Use google()/mavenCentral() for local builds and only switch to
the Azure Artifacts feed when TF_BUILD=True (Azure Pipelines).

The init.gradle still handles Android SDK bindings Gradle targets
redirection in CI via the pipeline template.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- eng/ingest-maven-deps.sh now exports TF_BUILD=True so Gradle
  resolves through the Azure Artifacts feed during ingestion
- Use equalsIgnoreCase for TF_BUILD check to handle any casing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…5219)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

## Problem

PR dotnet#35169 switched all Gradle repos to the Azure Artifacts feed
unconditionally. This causes **10+ minute local Android builds** because
the credential provider plugin adds significant auth overhead (MSAL
token acquisition, feed latency vs direct Maven Central).

Reported by team members in India experiencing builds that never
complete.

## Fix

Use `google()`/`mavenCentral()` for local builds and only switch to the
Azure Artifacts feed when `TF_BUILD=True` (Azure Pipelines with CFSClean
network isolation).

```groovy
if (System.getenv('TF_BUILD') == 'True') {
    // Azure Artifacts feed for CI
} else {
    // Standard Maven repos for local dev
}
```

This also removes the credential provider plugin dependency for local
builds since it's not needed when using standard Maven repos.

The `init.gradle` still handles the Android SDK bindings Gradle targets
redirection in CI via the pipeline template.

## Verified
- ✅ Local build uses standard Maven repos (fast, no credential provider
overhead)
- ✅ CI sets `TF_BUILD=True` which activates the Azure Artifacts feed
- Remove Checkout-GhAwPr.ps1 (116 lines) — checkout not needed,
  agent reads PR data via MCP tools
- Remove gh-aw-workflows.instructions.md (replaced by gh-aw-guide skill)
- Simplify copilot-evaluate-tests.md:
  - Remove checkout step (agent uses MCP tools, not filesystem)
  - Add roles: [admin, maintain, write] (was missing)
  - Fix cancel-in-progress: true → false (slash_command best practice)
  - Update prompt to reference MCP tools instead of local files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

## Description

Simplifies the evaluate-tests gh-aw workflow and removes obsolete files.

### Changes

**Workflow improvements (`copilot-evaluate-tests.md`):**
- Remove checkout step — agent reads PR data via MCP tools, not
filesystem
- Add `roles: [admin, maintain, write]` (was missing — security gap)
- Fix `cancel-in-progress: true` → `false` (slash_command best practice
per gh-aw guide)
- Update prompt to reference MCP tools instead of local files

**Removed files:**
- `Checkout-GhAwPr.ps1` (116 lines) — no longer needed since workflows
use MCP tools
- `gh-aw-workflows.instructions.md` (290 lines) — replaced by the
gh-aw-guide skill
…5106)

## Summary

Add guidance to always use the `ci-analysis` skill when investigating CI
failures instead of manual AzDO API queries.

## Motivation

During the net11.0 stabilization work, we repeatedly made incomplete CI
assessments by manually querying AzDO APIs — missing Helix work item
failures, not cross-referencing known build errors, and giving incorrect
all-clear signals. The `ci-analysis` skill handles all of this
automatically but was not being used consistently.

## Changes

### `copilot-instructions.md`
- New **Investigating CI Failures** section under CI Pipelines
- Mandates `ci-analysis` skill for all CI status checks
- Documents what the skill provides (Helix logs, known issue matching,
cross-build aggregation)
- Lists trigger phrases and anti-patterns
- Documents XHarness exit-0 blind spot for `maui-pr-devicetests` with
cross-check guidance
- Adds escalation path to `helix-investigation` and
`azdo-build-investigator` skills

## Context

dotnet/runtime has no equivalent CI investigation guidance in their
copilot instructions — this is MAUI-specific due to our multi-pipeline
setup (maui-pr + maui-pr-uitests + maui-pr-devicetests) and Helix test
infrastructure.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

## Description

Removes the Expert Code Review (`/review`) gh-aw workflow. Will be
re-added with improvements from PR dotnet#35220.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ability (dotnet#35133)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

> **Depends on dotnet#35136** (pipeline category detection — should merge
first)

## What this does

Two things:

### 1. UI test category detection in PR review

During the PR review workflow, Step 0.5 detects which UI test categories
the PR impacts and writes the result to the AI summary comment. This
gives reviewers visibility into which UI tests are relevant.

**Detection** reuses the 3-tier script from dotnet#35136 (test attributes →
source paths → AI reasoning).

**AI summary** shows a new 🧪 UI Tests section with detected categories
before the gate section.

### 2. Gate reliability fixes

Multiple fixes to make the gate (`verify-tests-fail.ps1`) more
deterministic:

| Fix | Problem it solves |
|-----|-------------------|
| **Absolute path resolution** | Gate scripts not found on Linux CI
agents (`Resolve-Path`, `GetFullPath`) |
| **File existence check** | Instant cryptic failure when verify script
is missing — now logs clear error |
| **3x retry on ENV ERROR** | Emulator timeouts, ADB failures, app
crashes — transient issues that pass on retry |
| **Strip bad report blocks** | Old verify script produces `Passed:
False` with empty counts — stripped instead of shown |
| **Gate log in fallback** | When report is missing, shows last 20 lines
of gate output instead of just `❌ FAILED / Platform: IOS` |

## Files

| File | Changes |
|------|---------|
| `.github/scripts/Review-PR.ps1` | Step 0.5 category detection + all 5
gate fixes |
| `.github/scripts/post-ai-summary-comment.ps1` | Add `uitests` phase to
render detected categories |
| `.github/pr-review/pr-preflight.md` | Step 7: AI identifies impacted
UI test categories |

## Validation — PR reviewer builds (Apr 26)

10 builds against real PRs — all succeeded ✅. Category detection shown
in AI summary comment.

| PR | Categories Detected | Build | AI Summary |
|----|-------------------|-------|------------|
| dotnet#35037 (WebView theme) | `ViewBaseTests,WebView` |
[13940071](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940071)
|
[comment](dotnet#35037 (comment))
|
| dotnet#35031 (Shell memory leak) | `Shell` |
[13940072](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940072)
|
[comment](dotnet#35031 (comment))
|
| dotnet#35020 (XAML Hot Reload) | _(none — XAML only)_ |
[13940073](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940073)
| ✅ Shows "No UI test categories" |
| dotnet#35008 (Shell SearchHandler) | `Shell` |
[13940074](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940074)
| ✅ |
| dotnet#34997 (RadioButton gradient) | `RadioButton,ViewBaseTests` |
[13940075](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940075)
| ✅ |
| dotnet#34980 (DatePicker rotation) | `ViewBaseTests` |
[13940076](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940076)
| ✅ |
| dotnet#34974 (Picker CharacterSpacing) | `ViewBaseTests` |
[13940077](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940077)
| ✅ |
| dotnet#34923 (SwipeView threshold) | `SwipeView,ViewBaseTests` |
[13940078](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940078)
| ✅ |
| dotnet#34907 (CollectionView ScrollTo) | `CollectionView` |
[13940079](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940079)
| ✅ |
| dotnet#34845 (RefreshView binding) | `RefreshView,ViewBaseTests` |
[13940080](https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=13940080)
| ✅ |

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This pull request updates the following dependencies

[marker]: <> (Begin:a71c12d9-5aa4-4b46-e2d6-08da0cf8cd95)
## From https://github.com/dotnet/xharness
- **Subscription**:
[a71c12d9-5aa4-4b46-e2d6-08da0cf8cd95](https://maestro.dot.net/subscriptions?search=a71c12d9-5aa4-4b46-e2d6-08da0cf8cd95)
- **Build**:
[20260429.1](https://dev.azure.com/dnceng/internal/_build/results?buildId=2963227)
([312418](https://maestro.dot.net/channel/2/github:dotnet:xharness/build/312418))
- **Date Produced**: April 29, 2026 1:57:22 PM UTC
- **Commit**:
[9d5a7e96236317f6312b25590e93ce6c363b62c3](dotnet/xharness@9d5a7e9)
- **Branch**: [main](https://github.com/dotnet/xharness/tree/main)

[DependencyUpdate]: <> (Begin)

- **Dependency Updates**:
  - From [11.0.0-prerelease.26107.1 to 11.0.0-prerelease.26229.1][1]
     - Microsoft.DotNet.XHarness.CLI
     - Microsoft.DotNet.XHarness.TestRunners.Common
     - Microsoft.DotNet.XHarness.TestRunners.Xunit

[1]: dotnet/xharness@bfbac23...9d5a7e9

[DependencyUpdate]: <> (End)


[marker]: <> (End:a71c12d9-5aa4-4b46-e2d6-08da0cf8cd95)

Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com>
- ICrossPlatformLayout.CrossPlatformArrange can call LayoutButton with a null button so it should be prepared for that.

Fixes dotnet#31048
@github-actions

github-actions Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35284

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35284"

@dotnet-policy-service dotnet-policy-service Bot added the community ✨ Community Contribution label May 2, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Hey there @@GamesAgeddon! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! Can you create a test for it to prove the bug?

@GamesAgeddon

GamesAgeddon commented May 2, 2026

Copy link
Copy Markdown
Contributor Author

@kubaflo It's very hard to reproduce in a real app. I need around 5 to 15 minutes every time navigating back and forth in our app, hoping that the timing causes this issue. In a test scenario, you can force it with these lines of code in any project targeting net10.0-ios.

var b = new Button();
((ICrossPlatformLayout)b).CrossPlatformArrange(Rect.Zero);

Because as seen in the screenshot Size ICrossPlatformLayout.CrossPlatformArrange handle safely if the handler is null var platformButton = Handler?.PlatformView as UIButton; but just call LayoutButton, which is not prepared for it, we see this exception.

[System.NullReferenceException thrown]	
>	Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Button.LayoutButton(UIKit.UIButton platformButton, Microsoft.Maui.Controls.Button button, Microsoft.Maui.Graphics.Rect size) Line 186	C#
 	Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Button.Microsoft.Maui.ICrossPlatformLayout.CrossPlatformArrange(Microsoft.Maui.Graphics.Rect bounds) Line 165	C#
 	CustomApp.Maui.iOS.dll!CustomApp.Maui.iOS.Handlers.IosButtonHandler.TryGetPlatformView(CustomApp.Maui.iOS.Handlers.IosButtonHandler handler) Line 68	C#
 	CustomApp.Maui.iOS.dll!CustomApp.Maui.iOS.Handlers.IosButtonHandler.PlatformArrange(Microsoft.Maui.Graphics.Rect rect) Line 28	C#
 	Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.VisualElement.ArrangeOverride(Microsoft.Maui.Graphics.Rect bounds) Line 2005	C#
 	Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.VisualElement.Microsoft.Maui.IView.Arrange(Microsoft.Maui.Graphics.Rect bounds) Line 1993	C#
 	Microsoft.Maui.dll!Microsoft.Maui.Layouts.GridLayoutManager.ArrangeChildren(Microsoft.Maui.Graphics.Rect bounds) Line 46	C#
 	Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Layout.CrossPlatformArrange(Microsoft.Maui.Graphics.Rect bounds) Line 384	C#
 	Microsoft.Maui.dll!Microsoft.Maui.Platform.MauiView.CrossPlatformArrange(CoreGraphics.CGRect bounds) Line 532	C#
 	Microsoft.Maui.dll!Microsoft.Maui.Platform.MauiView.LayoutSubviews() Line 622	C#
 	Microsoft.iOS.dll!ObjCRuntime.Messaging.void_objc_msgSend_bool(nint receiver, nint selector, byte arg1)	Unknown
 	Microsoft.iOS.dll!ObjCRuntime.Messaging.void_objc_msgSend_bool(nint receiver, nint selector, byte arg1)	Unknown
 	Microsoft.iOS.dll!UIKit.UIApplication.StatusBarHidden.set(bool value)	Unknown
 	CustomApp.Maui.iOS.dll!CustomApp.Maui.iOS.Delegates.AppDelegate.FinishedLaunching(UIKit.UIApplication uiApplication, Foundation.NSDictionary launchOptions) Line 264	C#
 	Microsoft.iOS.dll!UIKit.UIApplication.xamarin_UIApplicationMain(int argc, nint argv, nint principalClassName, nint delegateClassName, nint* gchandle)	Unknown
 	Microsoft.iOS.dll!UIKit.UIApplication.UIApplicationMain(int argc, string[] argv, nint principalClassName, nint delegateClassName)	Unknown
 	Microsoft.iOS.dll!UIKit.UIApplication.Main(string[] args, System.Type principalClass, System.Type delegateClass)	Unknown
image

@kubaflo
kubaflo changed the base branch from main to inflight/current May 2, 2026 14:19
@kubaflo
kubaflo merged commit 12921f7 into dotnet:inflight/current May 2, 2026
2 of 4 checks passed
@github-actions github-actions Bot added this to the .NET 10 SR7 milestone May 2, 2026
github-actions Bot pushed a commit that referenced this pull request May 6, 2026
- ICrossPlatformLayout.CrossPlatformArrange can call LayoutButton with a
null button so it should be prepared for that.

Fixes #31048

---------
mattleibow added a commit to mattleibow/DeviceRunners that referenced this pull request May 8, 2026
MAUI 10.0.20 had a NullReferenceException in Button.LayoutButton when
the platform button was null during a layout pass on the UI thread.
This crashed the app mid-test-run on iOS, producing only partial results.

The fix (dotnet/maui#35284, fixing dotnet/maui#31048) adds a null guard
in LayoutButton. MAUI 10.0.60 includes this fix.

Results: iOS now completes all 44 tests (previously crashed at 8).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
mattleibow added a commit to mattleibow/DeviceRunners that referenced this pull request May 8, 2026
MAUI 10.0.20 had a NullReferenceException in Button.LayoutButton when
the platform button was null during a layout pass on the UI thread.
This crashed the app mid-test-run on iOS, producing only partial results.

The fix (dotnet/maui#35284, fixing dotnet/maui#31048) adds a null guard
in LayoutButton. MAUI 10.0.60 includes this fix.

Results: iOS now completes all 44 tests (previously crashed at 8).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
mattleibow added a commit to mattleibow/DeviceRunners that referenced this pull request May 15, 2026
…ntegration (#110)

* Add dotnet test support for Android and iOS via DeviceRunners.Testing.Targets

Introduces a new MSBuild targets NuGet package that makes dotnet test work
for Android and iOS MAUI test apps with a single command:

  dotnet test MyTests.csproj -f net10.0-android

New: DeviceRunners.Testing.Targets NuGet package
- build/DeviceRunners.Testing.Targets.props injects DEVICE_RUNNERS_AUTORUN,
  DEVICE_RUNNERS_PORT, DEVICE_RUNNERS_HOST_NAMES into the app bundle at build
  time via AndroidEnvironmentVariables / iOSEnvironmentVariables MSBuild items
- build/DeviceRunners.Testing.Targets.targets overrides the VSTest target for
  Android/iOS to invoke the bundled CLI tool; selects the correct self-contained
  binary for the host RID (win-x64, linux-x64, osx-x64, osx-arm64)
- PublishCliTools MSBuild target publishes the CLI self-contained + trimmed for
  all 4 RIDs before packing the NuGet package

New: UseTestRunnerEnvironment() extension method
Replaces the compile-time #if MODE_NON_INTERACTIVE_VISUAL block with a runtime
env var check. When DEVICE_RUNNERS_AUTORUN is set (injected by the .targets file),
the app auto-enables headless mode with TCP result streaming.

Updated: sample test project
- Removes #if MODE_NON_INTERACTIVE_VISUAL and TestingMode/MODE_NON_INTERACTIVE_VISUAL
- Calls UseTestRunnerEnvironment() instead (no-op when env var is absent)
- Imports DeviceRunners.Testing.Targets .props and .targets directly for in-repo use

Updated: CI workflows (test-tcp-android-linux, test-tcp-ios)
- Replaces 2-step dotnet publish + device-runners CLI with a single dotnet test call
- Adds PublishCliTools step to build bundled native binaries before dotnet test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Use dotnet pack instead of dotnet msbuild in CI to publish CLI binaries

dotnet pack triggers PublishCliTools via BeforeTargets=GenerateNuspec, which
is the natural way to prepare the NuGet package and its bundled binaries.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Android env var injection and iOS launch env vars for dotnet test support

- Replace non-existent AndroidEnvironmentVariables ItemGroup with correct
  _GeneratedAndroidEnvironment items in a BeforeTargets=_GenerateEnvironmentFiles target.
  The Android SDK compiles these into the native libxamarin-app.so so that Mono
  makes them available via Environment.GetEnvironmentVariable() at app startup.

- Use %3B to escape semicolons in HOST_NAMES value, preventing MSBuild from
  treating the list separator as an item separator.

- Update iOS LaunchAppAsync to accept environmentVariables parameter and inject
  them using the xcrun simctl SIMCTL_CHILD_* mechanism when provided.

- Update iOS TestCommand to pass DEVICE_RUNNERS_* env vars at app launch time
  (matching how Windows and macOS CLI commands already inject them).

- Add DeviceRunners.Cli FDD publish output to tools/ for in-repo testing.

Android dotnet test now works end-to-end: 44 tests, 36 pass, 5 intentional fail,
TRX written, correct non-zero exit code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix review findings and .app bundle glob for macOS/iOS dotnet test

- Fix inaccurate comment in MacOSService: 'open --env' does support env
  vars; direct launch is used for process handle retention and simplicity
- Read stderr on xcrun simctl launch failure for better CI diagnostics
- Make iOS CI dotnet pack use 'release' consistently with macOS CI
- Fix .app bundle discovery: MSBuild Include glob (*.app/) does not
  reliably match directories on macOS; use Directory.GetDirectories
  instead — verified working for both iOS and macOS Catalyst
- Remove committed binary DLLs from tools/ and add .gitignore so
  they are regenerated by dotnet pack (PublishCliTools target)
- Validated end-to-end: dotnet test works for macOS Catalyst (44 tests,
  TRX generated) and iOS simulator (44 tests, TRX generated)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update AzDO pipelines to use dotnet test via DeviceRunners.Testing.Targets

Mirror the same simplification already applied to GitHub Actions:
- Replace 'dotnet tool install + dotnet publish + device-runners <plat> test'
  with 'dotnet pack Testing.Targets + dotnet test'
- Remove TestingMode=NonInteractiveVisual (replaced by env var auto-config)
- Remove manual .app/.apk/.exe discovery steps
- All four platforms updated: Android, iOS, macOS, Windows

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add global.json pinned to .NET 10.0.x and skip Xcode version check

- global.json: pin SDK to 10.0.100 with rollForward=latestFeature
  so any 10.0.* SDK satisfies the requirement.
- Directory.Build.props: set ValidateXcodeVersion=false so minor
  Xcode version mismatches don't break builds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Improve dotnet test output: replace MSB3073 with human-readable summary

Instead of the raw MSB3073 'command exited with code 1' error:
- All Exec tasks now use IgnoreExitCode=true and capture the exit code
- New _DeviceRunnersReportResults target parses the TRX via XmlPeek
  and emits a dotnet-test-style summary line, e.g.:
    Failed! - Failed: 5, Passed: 36, Skipped: 3, Total: 44
- On failure, emits a clean Error task message instead of MSB3073
- Fallback error for the case where CLI crashes before writing TRX
- Fix: count skipped via UnitTestResult[@outcome='NotExecuted'] elements
  because xunit/nunit skipped tests don't increment Counters/@notExecuted

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Restructure MSBuild targets: DependsOnTargets chain, shared args, single Exec

Replace CallTarget with proper DependsOnTargets chains and eliminate
duplicate Exec blocks. New target hierarchy:

  VSTest
    → _DeviceRunnersCheckCli  (fail fast before building)
    → Build
    → _DeviceRunnersRunTests
        → _DeviceRunnersPrepareArgs
            → _DeviceRunnersCommonArgs   (TRX path, device flag, shared CLI flags)
            → _DeviceRunnersAndroidArgs  (Condition=android)
            → _DeviceRunnersiOSArgs      (Condition=ios)
            → _DeviceRunnersMacOSArgs    (Condition=maccatalyst)
            → _DeviceRunnersWindowsArgs  (Condition=windows)
        → _DeviceRunnersExecTests        (single Exec, consumes $(_DeviceRunnersCliArgs))
        → _DeviceRunnersReportResults    (XmlPeek TRX, emit summary)

Each platform arg target sets $(_DeviceRunnersCliArgs); the optional
$(_DeviceRunnersDeviceArg) is built once in _DeviceRunnersCommonArgs and
appended where the CLI supports it (Android, iOS). No duplicate Exec pairs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: restructure MSBuild targets with DependsOnTargets chains

- Replace CallTarget usage with proper DependsOnTargets chains
- VSTest -> _DeviceRunnersCheckCli;Build;_DeviceRunnersRunTests
- _DeviceRunnersRunTests -> _DeviceRunnersPrepareArgs;_DeviceRunnersExecTests;_DeviceRunnersReportResults
- _DeviceRunnersPrepareArgs -> _DeviceRunnersCommonArgs + 4 platform targets
- Introduce _DeviceRunnersPlatform and _DeviceRunnersIsSupportedPlatform properties
  so all conditions use property comparisons instead of repeated function calls
- Single _DeviceRunnersExecTests consumes fully-assembled $(_DeviceRunnersCliArgs)
- Single _DeviceRunnersDeviceArg with leading space for clean concatenation
- ASCII-only comments (no box-drawing chars, no -- inside XML comments)

Validated: dotnet test macOS Catalyst - 44 tests (36 pass, 5 fail, 3 skip)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: match dotnet test output format for results summary

Emit the standard dotnet test two-line format:
  Results File: /path/to/test-results.trx
  Test summary: total: 44, failed: 5, succeeded: 36, skipped: 3, duration: 9.3s

Changes:
- Output 'Results File:' line first (before summary, not after)
- Use 'succeeded' instead of 'passed' to match dotnet test terminology
- Read scalar TRX counters as PropertyName (not ItemName) for clean interpolation
- Compute duration from TRX Times/@start and Times/@finish attributes
- Keep bare 'Test run failed.' Error as the MSBuild failure signal

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: use TESTERROR error code to match dotnet test output

Adds Code="TESTERROR" to the two test-failure Error tasks so the
output matches what dotnet test emits for failing tests:

  error TESTERROR: Test run failed.

Tools that parse dotnet test output keyed on the TESTERROR code will
now correctly recognise device test failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: embed test summary in TESTERROR for terminal logger visibility

The dotnet CLI terminal logger (default in .NET 8+) filters MSBuild
Message tasks, so Results File and Test summary were invisible at
default verbosity. Fix by embedding the full summary in the TESTERROR
error message which is always surfaced:

  error TESTERROR: Test run failed: 5 of 44 tests failed
    (36 succeeded, 3 skipped, duration: 9.7s). Results File: /path/...

The Message tasks are kept for --tl:off / --verbosity normal users
where they render as the clean two-line dotnet test format:
  Results File: /path/to/test-results.trx
  Test summary: total: 44, failed: 5, succeeded: 36, skipped: 3, duration: 9.7s

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: emit Results File and Test summary as separate TESTERROR lines

Use %0A (MSBuild newline escape) to split the error into two lines
matching the standard dotnet test output format:

  error TESTERROR: Results File: /path/to/test-results.trx
  error TESTERROR: Test summary: total: 44, failed: 5, succeeded: 36, skipped: 3, duration: 18.5s

Both lines are always visible via the terminal logger.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: drop Results File from TESTERROR, keep Test summary only

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: preserve test assemblies from iOS/Android linker

Test classes are discovered via reflection at runtime. Without explicit
trimmer roots the iOS/Android linker strips the referenced test assemblies
(MauiLibrary.XunitTests, Library.NUnitTests), causing only tests in the
main DeviceTests assembly to run (~8 instead of ~44).

TrimmerRootAssembly tells the linker to keep the full assembly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: detect app crash during test run via missing end event

The event stream protocol sends begin/end events to bracket the test run.
If the TCP connection closes after receiving results but without the end
event, the app crashed or was killed mid-run.

Previously this produced partial results with no indication of a crash.
Now the CLI explicitly reports:

  The application appears to have crashed during the test run.
  Only 8 test result(s) were received before the connection was lost.
  Check the device log for crash details.

And forces a non-zero exit code even if all received tests passed, so
CI pipelines correctly fail.

Changes:
- EventStreamService: add HasStarted and HasEnded tracking
- BaseTestCommand: check for begin-without-end after listener completes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: use CLI exit code 2 for app crash detection

CLI exit codes:
  0 = all tests passed
  1 = test failures
  2 = app crashed (begin event received, results streamed, but no end event)

MSBuild targets check the exit code directly (no file reading needed):
  exit code 2 + TRX:    'Test summary: ... (incomplete: app crashed)'
  exit code 2 + no TRX: 'The application crashed before producing any test results'
  exit code 1 + TRX:    'Test summary: ...' (normal failure)

Validated:
  iOS:   8 tests, crash detected, '(incomplete: app crashed)' shown
  macOS: 44 tests, no crash, normal TESTERROR shown

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: replace tuple return with TestListenerResult record

Replace the (int testFailures, string? testResults) tuple with a
protected record TestListenerResult(int FailedCount, string? ResultsFile, bool Crashed).

This makes the crash detection explicit via a bool rather than a
sentinel value, and gives clear property names at all call sites.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: add ToExitCode() and Success to TestListenerResult

Centralise exit code mapping and success logic on the record itself:
  - ToExitCode(): 0 = success, 1 = test failures, 2 = app crashed
  - Success: true when no failures and no crash

All callers now use listener.ToExitCode() and listener.Success instead
of duplicating the conditional logic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: bump MAUI to 10.0.60 to fix Button.LayoutButton NRE on iOS

MAUI 10.0.20 had a NullReferenceException in Button.LayoutButton when
the platform button was null during a layout pass on the UI thread.
This crashed the app mid-test-run on iOS, producing only partial results.

The fix (dotnet/maui#35284, fixing dotnet/maui#31048) adds a null guard
in LayoutButton. MAUI 10.0.60 includes this fix.

Results: iOS now completes all 44 tests (previously crashed at 8).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: filter iOS device log to app process for managed exception details

Use the existing AppleDev predicate parameter on GetLogsPlainAsync to
filter logs to just the app process:

  predicate: 'process == "DeviceTestingKitApp.DeviceTests"'

Results:
  Before: 17,000 lines of OS-level noise, no managed exceptions
  After:  190 lines with all PASS/FAIL output and full managed stack
          traces when the app crashes (NullReferenceException, etc.)

Also restores MAUI 10.0.60 (was temporarily reverted for testing).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: bump AppleDev 0.8.7->0.8.10, AndroidSdk 0.34.0->0.35.1

AppleDev 0.8.10 adds SimCtl.LaunchAppAsync overload with env vars,
so we can replace the manual ProcessStartInfo + SIMCTL_CHILD_* workaround
with the native API call.

AndroidSdk 0.35.1 is the latest stable release.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add WindowsPackageType=MSIX to packaged Windows CI workflows

The Testing.Targets .props defaults WindowsPackageType=None (for dotnet test
unpackaged mode). The TCP Windows packaged and XHarness Windows workflows
need MSIX packaging, so pass -p:WindowsPackageType=MSIX explicitly.

Fixes: TCP Windows (GH+AzDO), XHarness Windows (GH+AzDO) CI failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: add dotnet test documentation as primary testing approach

New articles:
- using-dotnet-test.md: Main guide covering setup, configuration,
  output format, crash detection, MSBuild properties, and how it works
- dotnet-test-android.md: Emulator setup, env var injection, APK config
- dotnet-test-ios.md: Simulator setup, device logs, crash troubleshooting
- dotnet-test-macos.md: Direct launch, no simulator needed
- dotnet-test-windows.md: Unpackaged EXE mode, WindowsPackageType

Updated:
- toc.yml: 'dotnet test (Recommended)' section at top, CLI renamed to
  'Advanced', XHarness renamed to 'Legacy' (from 'CLI Runners')
- index.md: Getting Started leads with dotnet test
- README.md: dotnet test as primary, links to docs site
- technical-architecture-overview.md: Testing.Targets package section
  with MSBuild target chain, exit code protocol, package structure
- ci-pipeline.md: dotnet test CI examples for GH Actions and AzDO

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: detect Windows package type instead of overriding it

Remove WindowsPackageType=None default from .props entirely. Instead,
the _DeviceRunnersWindowsArgs target detects what the build produced:

- If .exe exists (unpackaged): uses 'windows test --app path.exe'
- If .msix exists (packaged): uses 'windows test --app path.msix'
- If neither: clear error message

This means dotnet publish for MSIX packaging is no longer broken by the
Testing.Targets package. CI workflows no longer need -p:WindowsPackageType
overrides. Also search both artifacts/ and sample/ for .msix in CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: extract DeviceRunners.VisualRunners.Core for xplat types

Create DeviceRunners.VisualRunners.Core (net9.0;net10.0 only) with the
cross-platform testing types that the CLI needs:

Moved from VisualRunners:
- Testing/ITestResultInfo, ITestCaseInfo, ITestAssemblyInfo,
  ITestAssemblyConfiguration, TestResultStatus
- Testing/Channels/IResultChannel, IResultChannelFormatter,
  FileResultChannel, FileResultChannelOptions, TestResultEvent,
  EventStreamFormatter, TextResultChannelFormatter,
  TrxResultChannelFormatter, TextWriterResultChannel

The CLI now references VisualRunners.Core instead of VisualRunners,
breaking the dependency on multi-target mobile TFMs. This unblocks
self-contained and AOT publish (no more Mono runtime pack errors).

VisualRunners references VisualRunners.Core transitively, so all
existing consumers are unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: enable trimmed single-file publish for CLI tool

Add TrimmerRootAssembly for Spectre.Console.Cli and DeviceRunners.Cli
to preserve types discovered via reflection (command settings classes).
Spectre.Console.Cli is explicitly not trim-safe (spectreconsole/spectre.console#1690)
so the entire assembly must be rooted.

Suppress trim analysis warnings since Spectre.Console.Cli generates
unavoidable IL2026/IL2090 warnings that are covered by the rooting.

Result: 19MB self-contained single-file binary (down from 21MB multi-file).
  dotnet publish -r osx-arm64 --self-contained -p:PublishSingleFile=true

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: default CLI to self-contained single-file trimmed publish

CLI csproj now has SelfContained, PublishSingleFile, PublishTrimmed,
and RuntimeIdentifiers (osx-arm64;win-x64) set by default. Plain
'dotnet publish -r osx-arm64' produces a 19MB single-file binary.

The NuGet package PublishCliTools target overrides these back to
framework-dependent (SelfContained=false, PublishSingleFile=false,
PublishTrimmed=false) since the bundled tool runs via 'dotnet <dll>'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: bundle per-RID single-file CLI binaries in NuGet package

The NuGet package now ships self-contained, trimmed, single-file native
binaries for each platform in tools/<rid>/:

  tools/osx-arm64/DeviceRunners.Cli     (~20 MB)
  tools/win-x64/DeviceRunners.Cli.exe   (~20 MB)
  tools/linux-x64/DeviceRunners.Cli     (~20 MB)

The MSBuild targets detect the host OS and select the right binary.
Falls back to framework-dependent DLL at tools/DeviceRunners.Cli.dll
for in-repo dev or older packages.

Changes:
- CLI csproj: add JsonSerializerIsReflectionEnabledByDefault, linux-x64 RID
- Testing.Targets csproj: PublishCliTools publishes 3 RIDs (self-contained)
- .targets: detect host OS -> pick tools/<rid>/DeviceRunners.Cli[.exe]

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: remove framework-dependent DLL fallback from targets

Only the per-RID single-file native binary is used now. The fallback
to tools/DeviceRunners.Cli.dll is removed since it will never exist.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: convert TCP Windows packaged workflow to dotnet test

Replace manual CLI tool install + device-runners commands with a single
dotnet test call, matching all other TCP workflows. Removes:
- dotnet pack + dotnet tool install of CLI global tool
- Manual cert install/uninstall via device-runners windows cert
- Manual dotnet publish + MSIX search + device-runners windows test

The Testing.Targets package handles build, deploy, and test collection.

All 10 TCP workflows (5 GH Actions + 5 AzDO) now use dotnet test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add pack step to CI, use WindowsPackageType, clean up section headings

CI fixes:
- Add 'Publish CLI Tool Binaries' (dotnet pack) step to macOS, Windows,
  and Windows Unpackaged workflows (GH Actions + AzDO) so the native
  CLI binary is available before dotnet test runs.

MSBuild targets:
- Windows args: use $(WindowsPackageType) == 'None' to distinguish
  unpackaged (.exe) vs packaged (.msix) instead of probing file existence.
- Clean up XML section headings from ugly '===...' padding to '=[ name ]='
- Remove auto-publish hack from _DeviceRunnersCheckCli

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: remove _DeviceRunnersCheckCli dev-only target

The NuGet package always ships the native binary. No need for a
validation target in shipping targets.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: gate Android env var injection on dotnet test only, remove stale comment

- _DeviceRunnersAndroidEnvironment now requires DeviceRunnersInjectEnvironment=true
  which is only set by _DeviceRunnersPreBuild (runs before Build in the VSTest chain).
  A plain 'dotnet build' no longer bakes DEVICE_RUNNERS_AUTORUN=1 into the APK.
- Remove stale reference to _DeviceRunnersCheckCli in VSTest comment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add display names for AzDO Windows job templates

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: detect host architecture for CLI RID, publish all 6 RIDs

Targets: detect host CPU architecture via RuntimeInformation.OSArchitecture
to pick the correct RID (osx-arm64 vs osx-x64, etc.). Fixes 'Bad CPU type
in executable' on Intel macOS CI runners.

NuGet package now ships 6 RIDs:
  osx-arm64, osx-x64, win-x64, win-arm64, linux-x64, linux-arm64

PublishCliTools uses /nodeReuse:false to prevent file locking between
sequential publishes on Windows.

TCP Windows workflow uses WindowsPackageType=None (unpackaged) since
dotnet test calls Build, not Publish, and MSIX requires Publish.
MSIX-from-build support tracked separately.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: restore CLI workflow for Windows packaged MSIX tests

dotnet test uses Build (not Publish) so it can't produce MSIX packages.
Restore the direct CLI approach for the packaged Windows test workflow:
  dotnet pack CLI -> dotnet tool install -> dotnet publish app -> device-runners test

The unpackaged Windows workflow continues to use dotnet test.
MSIX-from-build support tracked separately.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: always pack Testing.Targets with release configuration

The CLI tool and targets package are tooling, not the app under test.
Always use -c release regardless of the test app's configuration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: note MSIX not yet supported via dotnet test, simplify Windows targets

- using-dotnet-test.md: comparison table shows MSIX as 'Coming soon'
- dotnet-test-windows.md: MSIX section with note and CLI redirect
- .targets: remove MSIX detection code, simplify to .exe only with
  clear error message mentioning WindowsPackageType=None

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: search project directory for MSIX AppPackages output

MSIX packaging puts output in <project>/AppPackages/, not artifacts/.
Search both the project directory and artifacts/ for all Windows
packaged workflows (TCP and XHarness, GH Actions and AzDO).

This was a pre-existing bug from main.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: revert MSIX search to artifacts only (matches main)

MSIX output lands at artifacts/bin/.../AppPackages/ which is already
under artifacts/. The recursive search finds it there.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: improve Windows .exe not found error message

Make it clear that dotnet test only supports unpackaged mode and
direct users to the CLI for MSIX. We never set WindowsPackageType
in the targets - we only read what the user configured.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: search entire repo for MSIX output

AppPackages output location varies depending on SDK version and
UseArtifactsOutput configuration. Search from repo root instead
of restricting to artifacts/ to handle all cases.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: filter MSIX search to AppPackages directories only

The broad recursive search from repo root was picking up runtime
dependency MSIX files (e.g. .WindowsAppRuntime.1.8.msix) instead
of the app MSIX. Filter to only match files in AppPackages dirs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: pass WindowsPackageType=None in unpackaged Windows CI workflows

The sample project doesn't set WindowsPackageType in its csproj, so it
defaults to MSIX packaging. The unpackaged CI workflows need to pass
-p:WindowsPackageType=None explicitly (matching what main does with
dotnet publish). Without this, the app builds as a packaged stub that
ignores ProcessStartInfo env vars, causing the TCP timeout.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: remove SelfContained/PublishSingleFile from CLI csproj

SelfContained=true in the csproj breaks dotnet pack/tool install
(the global tool becomes self-contained which doesn't work). Move
these flags to the PublishCliTools command line instead, where they
only apply to the NuGet package binary publish.

PublishTrimmed stays in the csproj (it only activates during publish).

This fixes TCP Windows packaged: the device-runners global tool was
being installed as self-contained, causing it to malfunction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: restore MODE_NON_INTERACTIVE_VISUAL for MSIX packaged Windows tests

UseTestRunnerEnvironment() reads env vars at runtime, but MSIX packaged
apps are launched via the package manager and don't receive env vars from
ProcessStartInfo. The packaged Windows CI workflow passes
-p:TestingMode=NonInteractiveVisual which defines MODE_NON_INTERACTIVE_VISUAL
and hardcodes the auto-start + TCP config at build time.

Both paths coexist: UseTestRunnerEnvironment() for dotnet test (unpackaged),
MODE_NON_INTERACTIVE_VISUAL for the CLI MSIX workflow.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add /nodeReuse:false to pack commands to prevent file locking

Multi-RID CLI publish during dotnet pack causes CS2012 file locking
errors on Windows CI runners. Adding /nodeReuse:false to the pack
command in both GH Actions and AzDO pipelines.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: add browser WASM platform support to Core and ConsoleResultChannel

- Add net9.0-browser/net10.0-browser TFMs to DeviceRunners.Core
- Add Platforms/Browser/DefaultAppTerminator.cs for WASM
- Update Platforms/All/DefaultAppTerminator.cs to exclude BROWSER
- Add ConsoleResultChannel to VisualRunners.Core for headless/WASM scenarios
- Add Platforms/Browser/ conditional compilation to Core .csproj

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: add DeviceRunners.VisualRunners.WebAssembly project

New project providing WASM test runner infrastructure:
- IWasmTestRunnerPlugin: extensible interface for test framework plugins
  (Xunit v2, v3, NUnit)
- WasmTestRunnerBuilder: fluent builder for configuring WASM test runs
  without MAUI dependency
- WasmTestRunner: orchestrates test execution and result channel lifecycle
- WasmTestRunResult: summary of test execution results
- Targets net9.0-browser and net10.0-browser

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: add CLI wasm test and serve commands

- WasmWebServerService: Kestrel-based static file server with WASM MIME types
  (.wasm, .dll, .pdb, .dat, .blat, .webcil) and cross-origin headers
- BrowserService: Playwright-based headless browser with console capture
- WasmTestCommand: end-to-end WASM test execution
  (serve → browser → console capture → EventStreamService → results)
- WasmServeCommand: utility to serve WASM apps for manual testing
- Add Microsoft.Playwright and ASP.NET Core framework references

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add FrameworkReference and fix Blazor component compilation

- Add Microsoft.AspNetCore.App FrameworkReference to csproj (required
  for Razor Class Libraries targeting net9.0/net10.0)
- Add component namespace imports to _Imports.razor so child components
  (HomePage, TestAssemblyPage, StatusBadge, etc.) resolve correctly
- Fix TestAssemblyPage: cast TestCases to INotifyCollectionChanged
  before subscribing to CollectionChanged (IList<T> does not expose it)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: add Xunit/NUnit WASM plugins and Blazor visual runner

Xunit WASM plugin (DeviceRunners.VisualRunners.Xunit.WebAssembly):
- XunitWasmTestRunnerPlugin: discovers and runs Xunit v2 tests in browser
- WasmXunitExecutionSink: captures test results for IResultChannel
- WasmTestRunnerBuilder.AddXunit() extension method

NUnit WASM plugin (DeviceRunners.VisualRunners.NUnit.WebAssembly):
- NUnitWasmTestRunnerPlugin: discovers and runs NUnit tests in browser
- WasmNUnitTestListener: captures test results for IResultChannel
- WasmTestRunnerBuilder.AddNUnit() extension method

Blazor visual test runner (DeviceRunners.VisualRunners.Blazor):
- Razor Class Library with Blazor components
- TestRunnerApp, HomePage, TestAssemblyPage, TestResultPage, DiagnosticsPage
- Reuses existing ViewModels from DeviceRunners.VisualRunners
- BlazorVisualRunnerExtensions for DI registration
- CSS styling for test status indicators

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: add sample WASM test app and dotnet test integration

Sample app (DeviceTestingKitApp.BrowserTests):
- Targets net9.0-browser with Xunit tests
- Uses WasmTestRunnerBuilder with AddXunit() plugin
- ConsoleResultChannel for NDJSON output to Console.Out
- Demonstrates passing, theory, and skipped tests

dotnet test integration (DeviceRunners.Testing.Targets):
- Add 'browser' to supported platforms
- _DeviceRunnersWasmArgs target: publishes WASM app, invokes CLI wasm test
- Configurable timeout (DeviceRunnersWasmTimeout) and browser
  (DeviceRunnersWasmBrowser) MSBuild properties
- Automatic wwwroot detection in publish output

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: handle CurrentUser cert store as fallback for non-admin MSIX testing

InstallCertificate, UninstallCertificate and IsCertificateInstalled now
try/check LocalMachine\TrustedPeople first and fall back to
CurrentUser\TrustedPeople. This allows running 'device-runners windows test'
without admin rights (the CLI no longer crashes); note that Add-AppxPackage
itself still requires the cert to be in LocalMachine\TrustedPeople on most
Windows configurations, so MSIX install will only succeed when the process
has admin rights or the cert is pre-installed at the machine level.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* ci: add WASM browser test jobs to GitHub Actions and Azure DevOps

GitHub Actions:
- Add wasm-browser job to ci.yml running on ubuntu-24.04
- Create test-wasm-browser composite action: publish → Playwright install → CLI wasm test
- Install wasm-tools workload in setup-tools

Azure DevOps:
- Add WASM_Browser_Tests stage to azure-pipelines.yml
- Create test-wasm-browser.yml template with same flow
- Install wasm-tools workload in setup-tools.yml

Both pipelines:
- Publish WASM app → find wwwroot → run device-runners wasm test
- Produce TRX results for test reporting
- Upload logs and test results as artifacts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: remove duplicate Blazor project entry from solution file

The solution file had DeviceRunners.VisualRunners.Blazor listed twice,
causing MSB4025 'Duplicate item' error on CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: use Blazor WebAssembly SDK for browser test sample

- Switch from Microsoft.NET.Sdk to Microsoft.NET.Sdk.BlazorWebAssembly
  so publish produces wwwroot with index.html and .wasm files
- Add Microsoft.AspNetCore.Components.WebAssembly package
- Add wwwroot/index.html with Blazor bootstrap script
- Update Program.cs to use WebAssemblyHostBuilder
- Add net9.0/net10.0 TFMs to WebAssembly plugin projects for Blazor compat
- Fix Playwright install step in CI (use playwright.ps1 from NuGet cache)
- Improve WASM publish path detection in CI (AppBundle, .wasm fallbacks)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert "fix: handle CurrentUser cert store as fallback for non-admin MSIX testing"

This reverts commit 8504d57b1a2ca22ce58c0b05eea8903412d02956.

* refactor: drop net9.0-browser TFMs, use plain net9.0 for WASM

Blazor WebAssembly uses plain net9.0 with Microsoft.NET.Sdk.BlazorWebAssembly,
not net9.0-browser. Remove the browser-specific TFMs and platform folders
since they aren't needed for the Blazor-based approach.

- Remove net9.0-browser/net10.0-browser from Core, WebAssembly, Xunit and NUnit projects
- Remove Platforms/Browser/ folder and conditional compilation from Core
- Remove wasm-tools workload from CI setup (not needed for Blazor WASM)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: use PowerShell for Playwright browser installation in CI

The bash-based playwright.ps1 invocation was failing with
MethodInvocationException. Switch to native pwsh shell for both
GitHub Actions and Azure DevOps templates.

Pack failure (CS2012 file lock) is a pre-existing Windows CI flake,
not related to this PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: replace Playwright with self-contained Chrome DevTools Protocol

Drop the Microsoft.Playwright dependency entirely. Instead, launch the
system-installed Chrome/Chromium directly and communicate via CDP over
WebSocket. This is the same approach XHarness uses (Selenium + CDP) but
without requiring any external tools to be installed.

BrowserService now:
- Finds Chrome/Chromium/Edge on the system (macOS, Linux, Windows)
- Supports CHROME_PATH environment variable override
- Launches headless with --remote-debugging-port=0 (auto-assign)
- Connects to CDP WebSocket and subscribes to Runtime.consoleAPICalled
- Navigates via Page.navigate CDP command
- Captures all console.log output as NDJSON events

Removed:
- Microsoft.Playwright NuGet package
- Playwright browser install steps from GitHub Actions and Azure DevOps
- --browser CLI option (Chrome/Chromium only via CDP)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: use reflection-based test runner for WASM instead of XunitFrontController

XunitFrontController requires filesystem access to load assemblies by path,
which is not available in browser WASM. Replace with a reflection-based
approach that discovers [Fact] and [Theory] methods from already-loaded
assemblies and executes them directly.

- Find test methods via reflection on [FactAttribute] and [InlineDataAttribute]
- Execute tests with proper async support and IDisposable cleanup
- Handle skip reasons, TargetInvocationException unwrapping
- Remove xunit.runner.utility dependency (only need xunit for attributes)
- Remove WasmXunitExecutionSink (no longer using XunitFrontController)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: replace ASP.NET Core web server with HttpListener

The FrameworkReference for Microsoft.AspNetCore.App combined with the
existing RuntimeIdentifiers caused NU1102 errors on CI — NuGet tried to
resolve ASP.NET Core runtime packs for all 6 RIDs (osx-arm64, win-x64, etc).

Replace the Kestrel-based WasmWebServerService with a lightweight
HttpListener implementation. Zero new dependencies — HttpListener is
built into .NET. The CLI csproj now has zero diff from the base branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: rename Windows CI jobs to (MSIX) and (EXE) matching main

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: add Windows MSIX support via loose deploy for dotnet test

Now that the CLI supports loose-file MSIX registration (via winapp.exe
from #94), dotnet test works for both Windows modes:

- Unpackaged (WindowsPackageType=None): detects .exe in build output
- Packaged (default MSIX): detects AppxManifest.xml in build output,
  CLI registers the app via winapp.exe loose deploy

TCP Windows (MSIX) CI workflows converted from manual CLI to dotnet test.
No more cert management, dotnet publish, or MSIX file search needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add developer mode and WinAppSDK runtime to Windows MSIX dotnet test

Loose-file MSIX registration requires Windows Developer Mode enabled
and the Windows App Runtime framework installed. These steps were in
the loose workflow but missing from the converted dotnet test workflow.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: separate CLI and dotnet test CI jobs

Restore test-tcp-* workflows to main's CLI-direct versions so the CLI
path continues to be tested. Add new test-dotnet-test-* workflows that
test the dotnet test integration via Testing.Targets.

New CI jobs (5 GH Actions + 5 AzDO):
- dotnet test Android (Linux)
- dotnet test iOS (x64)
- dotnet test macOS (x64)
- dotnet test Windows (MSIX) - loose deploy
- dotnet test Windows (EXE) - unpackaged

Existing CLI jobs restored from main:
- TCP Android, iOS, macOS, Windows (MSIX), Windows (EXE), Windows (Loose)

Both paths are now tested independently in CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: rename UseTestRunnerEnvironment to AddEnvironmentVariables

PR #111 landed the method as AddEnvironmentVariables, not
UseTestRunnerEnvironment. Fix MauiProgram.cs and docs to match.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: restore RootMode=all on TrimmerRootAssembly entries

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: use PublishForTargetsPackage property for CLI publish mode

Instead of passing --self-contained and -p:PublishSingleFile=true on the
command line, the CLI csproj uses a PublishForTargetsPackage property to
gate SelfContained, PublishSingleFile, and PublishTrimmed. The project
decides what's best for each publish mode:

  -p:PublishForTargetsPackage=true  -> self-contained, single-file, trimmed
  (default)                         -> framework-dependent tool

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address review findings (CLI args, Android env, .app lookup, docs, TRX)

1. Rename AddEnvironmentVariables -> AddCliConfiguration: reads env vars
   first, then falls back to CLI args (--device-runners-autorun etc.).
   This enables MSIX packaged Windows apps where env vars can't be
   forwarded but winapp.exe --args can pass CLI arguments.
   AddEnvironmentVariables kept as deprecated alias.

2. Windows loose launch now passes --device-runners-* CLI args via
   winapp.exe --args so MSIX apps auto-start without MODE_NON_INTERACTIVE.

3. Android env var injection: always inject when Testing.Targets is
   referenced (remove DeviceRunnersInjectEnvironment gate). Fixes stale
   APK issue with incremental builds.

4. iOS/macOS .app lookup: use $(AssemblyName).app instead of *.app to
   avoid matching multiple .app directories in the output.

5. Remove _DeviceRunnersPreBuild target (no longer needed).

6. Fix stale docs: remove _DeviceRunnersCheckCli references, update
   method names, fix package layout description.

7. Add TRX test-reporter to all dotnet test GH Actions jobs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: unify WASM with MAUI visual runner pattern

Delete redundant WebAssembly projects that duplicated existing abstractions:
- Remove DeviceRunners.VisualRunners.WebAssembly (IWasmTestRunnerPlugin, WasmTestRunnerBuilder)
- Remove DeviceRunners.VisualRunners.Xunit.WebAssembly
- Remove DeviceRunners.VisualRunners.NUnit.WebAssembly

Instead, add WASM-compatible implementations of the standard interfaces
directly in DeviceRunners.VisualRunners.Xunit:
- XunitWasmTestDiscoverer (ITestDiscoverer) — reflection-based discovery
- XunitWasmTestRunner (ITestRunner) — reflection-based execution
- AddXunitWasm() extension on IVisualTestRunnerConfigurationBuilder

The sample app now mirrors the MAUI pattern exactly:
  builder.Services.AddBlazorVisualTestRunner(conf => {
      conf.AddTestPlatform<XunitWasmTestDiscoverer, XunitWasmTestRunner>();
      conf.AddTestAssembly(typeof(Tests).Assembly);
      conf.EnableAutoStart(autoTerminate: true);
      conf.AddResultChannel(_ => new ConsoleResultChannel(new EventStreamFormatter()));
  });
  builder.RootComponents.Add<TestRunnerApp>("#app");

Same binary works both interactively (visual UI) and headlessly (CLI).

Also:
- Fix Blazor csproj: use PackageReference for ASP.NET Components instead
  of FrameworkReference (no browser-wasm runtime pack for ASP.NET Core)
- Fix AddBlazorVisualTestRunner to accept IVisualTestRunnerConfigurationBuilder
- Merge mattleibow/investigate-dotnet-test-mobile (3 commits)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add dotnet restore before WinAppSDK install in Windows MSIX workflows

The WinAppSDK runtime install step needs the NuGet package cache to be
populated. Add an explicit dotnet restore of the test project before
the install step to ensure microsoft.windowsappsdk is in the cache.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* revert: restore MAUI 10.0.20 to match main

The MAUI 10.0.60 bump may have changed MSIX output paths, breaking the
TCP Windows (MSIX) CI workflow. Revert to main's version (10.0.20).
MAUI version bump can be done in a separate PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: use xunit SDK internals for proper WASM test discovery and execution

Replace naive reflection-based [Fact]/[InlineData] scanning with xunit's
own discovery and execution engine, adapted for single-threaded WASM:

- WasmXunitDiscoverer: extends XunitTestFrameworkDiscoverer with
  threadless discovery via SynchronousMessageBus (handles Theory,
  MemberData, ClassData, all data sources)
- WasmXunitAssemblyRunner/CollectionRunner/ClassRunner: yield-based
  execution chain for cooperative multitasking in single-threaded WASM
- WasmExecutionSink: proper result collection via xunit message events
- Updated XunitWasmTestCaseInfo to wrap ITestCase instead of raw
  reflection types, enabling full xunit feature support
- Updated XunitWasmTestResultInfo to use ITestResultMessage with
  proper error/stack trace combining via ExceptionUtility
- Added comprehensive sample tests (MemberData, async, IDisposable,
  ITestOutputHelper) verifying all features work end-to-end
- Clean Program.cs using AddXunitWasm() extension

Based on XHarness ThreadlessXunitTestRunner and YieldingXunit2 patterns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add BuildInParallel=false to pack to prevent file locking

The multi-RID CLI publish during dotnet pack causes CS2012 file locking
when parallel TFM builds (net9.0 + net10.0) compete for the same output
files. /nodeReuse:false alone is insufficient because the outer pack
spawns MSBuild nodes that the inner publish commands conflict with.
BuildInParallel=false serializes the TFM builds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: publish only host RID in CI dotnet test workflows

Replace 'dotnet pack Testing.Targets' (which publishes all 6 RIDs) with
a single 'dotnet publish CLI -r <host-rid>' in each workflow. Publishing
6 RIDs on a Linux runner takes 30+ minutes (cross-compiling Windows
runtime packs) and was causing the Android job to time out.

Each workflow now publishes only what it needs:
- Android/Linux: linux-x64
- iOS/macOS: osx-x64
- Windows: win-x64

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: consolidate xunit types and unify Blazor/MAUI API

Consolidation:
- Delete duplicated Wasm wrapper types (XunitWasmTestCaseInfo, etc.)
  and reuse the existing XunitTestCaseInfo, XunitTestResultInfo,
  DeviceExecutionSink, XunitTestAssemblyInfo from the parent
- Rename Wasm/ → Reflection/ (names what it IS, not where it's used)
- XunitReflectionTestDiscoverer/Runner: threadless, reflection-based
  xunit discovery and execution, usable on any platform
- AddXunit(useReflection: true) parameter instead of separate method

API unification:
- BlazorVisualTestRunnerConfigurationBuilder → VisualTestRunnerConfigurationBuilder
- AddBlazorVisualTestRunner → UseVisualTestRunner (mirrors MAUI API)
- Extension target: WebAssemblyHostBuilder (like MauiAppBuilder)

Tests:
- Add unit tests for reflection-based discoverer and runner

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: remove AddEnvironmentVariables backward compat alias

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: disable shared Roslyn compilation during pack to prevent file locking

The VBCSCompiler server process holds DLLs open between sequential
dotnet publish commands within the PublishCliTools target. Adding
UseSharedCompilation=false forces each compilation to use its own
process instead of the shared server, preventing file locking.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add UseSharedCompilation=false to inner publish commands too

The outer dotnet pack properties don't propagate to inner Exec'd
dotnet publish commands since they spawn separate processes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: consolidate result channels and clean up xunit utilities

Result channels:
- Move ConsoleResultChannel and DebugResultChannel to VisualRunners.Core
- Default to TextResultChannelFormatter (human-readable), accept optional
  formatter parameter for NDJSON or other formats
- Delete duplicate internal copies from VisualRunners

Xunit reflection utilities:
- Delete NullMessageSink — replace with ConsoleDiagnosticMessageSink that
  routes xunit diagnostic messages to IDiagnosticsManager
- Rename NullSourceInformationProvider → EmptySourceInformationProvider
  (not null — it returns empty source info, which is correct when PDBs
  aren't available)
- Rename ReflectionXunitDiscoverer → XunitReflectionDiscoverer (consistent
  Xunit prefix)
- Consolidate utilities into XunitReflectionUtilities.cs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: remove RuntimeIdentifiers from CLI csproj to prevent cross-RID locking

RuntimeIdentifiers (plural) causes dotnet publish -r <rid> to evaluate
ALL listed RIDs during restore/build, not just the requested one. This
means sequential publishes for different RIDs conflict because the
linker from the previous RID still holds files open when the next RID
tries to re-evaluate the same paths.

Each dotnet publish -r <rid> in PublishCliTools already specifies the
RID explicitly, so RuntimeIdentifiers in the csproj is unnecessary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: split XunitReflectionUtilities into separate files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: remove RuntimeIdentifiers, simplify sequential per-RID publish

RuntimeIdentifiers (plural) in the csproj caused each dotnet publish -r
to evaluate ALL RIDs, leading to cross-RID file locking. Without it,
each -r flag builds only the requested RID.

Simplified PublishCliTools to inline the args per Exec (no shared
property). Removed BuildInParallel/UseSharedCompilation workarounds
from ci.yml since they were masking the real issue.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: unify DiagnosticMessageSink — delete ConsoleDiagnosticMessageSink

Add IMessageSink implementation and IDiagnosticsManager constructor to
the existing DiagnosticMessageSink instead of maintaining a separate class.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: simplify DiagnosticMessageSink to single IDiagnosticsManager constructor

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: correct double-encoded XML entities in Exec commands

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: flatten Reflection/ folder and rename Yielding* → XunitYielding*

- Move all files from Reflection/ to project root — no need for subfolder
- YieldingXunitAssemblyRunner → XunitYieldingAssemblyRunner
- YieldingXunitCollectionRunner → XunitYieldingCollectionRunner
- YieldingXunitClassRunner → XunitYieldingClassRunner

Consistent Xunit prefix across all types.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: use item batching for per-RID CLI publish

Define RIDs as an ItemGroup and use %(_CliTargetRid.Identity) in a
single Exec element. MSBuild batches the Exec once per item, running
each RID sequentially. Cleaner than 6 copy-pasted Exec commands.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: remove nodeReuse workaround, revert XHarness changes to main

nodeReuse:false was a workaround for RuntimeIdentifiers cross-RID
evaluation which no longer applies. Reverted XHarness Windows workflows
to match main exactly (display name and MSIX search path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: fix stale and incorrect documentation across all .md files

Key fixes:
- using-dotnet-test.md: AddCliConfiguration reads env vars AND CLI args
  (not just env vars), updated config injection table for Windows MSIX,
  removed contradictory notes about MSIX not being supported, updated
  How It Works to mention loose MSIX layout
- dotnet-test-macos.md: fixed glob pattern (.app not *.app)
- dotnet-test-windows.md: detection table now shows AppxManifest.xml
  (loose MSIX layout) instead of .msix, clarified CLI args for MSIX
- dotnet-test-ios.md: completed truncated Common causes section
- ci-pipeline.md: added all 10 test-dotnet-test-* workflows to file
  reference tables, added dotnet test column to platform matrix, fixed
  incorrect claim that TCP workflows use dotnet test, fixed AzDO example
  line continuation (backtick -> backslash)
- technical-architecture-overview.md: updated package structure from
  framework-dependent DLL to per-RID self-contained binaries, fixed
  Windows support indicator, updated launch description

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: support AddCliConfiguration for WASM via URL query params

The CLI navigates to ?device-runners-autorun=1 to trigger headless mode.
The app reads the current URL via JSImport and parses the query string
during builder configuration. When the param is absent (manual browser
open), auto-start is not enabled and the interactive visual runner shows.

- AddCliConfiguration(string currentUrl) for WASM in Blazor extensions
- CLI WasmTestCommand passes ?device-runners-autorun=1 in the URL
- index.html exposes getLocationHref() JS function for JSImport
- Sample Program.cs mirrors MAUI pattern: AddCliConfiguration + AddXunit
- AllowUnsafeBlocks for JSImport support

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: move BrowserInterop to Blazor library, auto-detect CLI in UseVisualTestRunner

- Move JSImport BrowserInterop class from sample to DeviceRunners.VisualRunners.Blazor
- UseVisualTestRunner automatically reads URL query params for CLI detection
  (no explicit AddCliConfiguration needed in the sample)
- AddCliConfiguration(string url) overload for WASM URL query param parsing
- Sample Program.cs is now clean — mirrors MAUI pattern exactly
- AllowUnsafeBlocks moved from sample to Blazor library (for JSImport)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: clarify AddCliConfiguration works with both CLI and dotnet test

Updated xmldoc, code comments, and docs to reflect that AddCliConfiguration
reads config injected by either the DeviceRunners CLI (direct launch) or
dotnet test (via MSBuild targets). Filed #123 for Android intent extras
as a follow-up to replace build-time env var injection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: call out Android build-time vs launch-time config injection

All platforms except Android inject configuration at launch time, allowing
the same built app to be reused with different settings. Android embeds
config into the APK at build time because adb has no env var mechanism.
References #123 for the planned intent extras improvement.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: move JS interop to library, align CI with existing patterns

JS interop:
- Move getLocationHref() JS to library wwwroot/device-runners-interop.js
  (shipped as RCL static asset at _content/.../)
- index.html references library JS instead of inline script

CI workflows (GitHub Actions + Azure DevOps):
- Match existing platform pattern: dotnet pack → dotnet tool install --global
- Use device-runners CLI as global tool (not dotnet run --project)
- Simplified app path detection matching macOS/iOS/Windows patterns
- Consistent step naming (Build and Install CLI Tool, Publish, Run Tests)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: include CLI tools in NuGet package via dynamic item inclusion

The static <None Include="tools\**"> glob was evaluated at project load
time, before PublishCliTools populated the tools/ directory. This meant
a clean pack produced a nupkg with build/ but zero tools/ files.

Fix: move the tools glob into the PublishCliTools target as a
_PackageFiles item, so it runs AFTER the per-RID publishes complete.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: reference library CSS in sample index.html

The CSS was shipped by the Blazor library but never referenced in the
host page. Add link to _content/.../device-runners.css, proper viewport
meta tag, and styled loading/error states.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: add Library.XunitTests, fix Blazor UI click-to-run and search

- Create DeviceTestingKitApp.Library.XunitTests: tests for shared library
  (CounterViewModel tests), mirroring MAUI's MauiLibrary.XunitTests
- BrowserTests now references multiple test assemblies (like MAUI DeviceTests)
- Fix: clicking a test now runs it first then navigates to result (matches MAUI)
- Fix: search filter error handling for WASM sync context
- Fix: CSS body margin/padding reset

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: align Android dotnet test workflow with working TCP workflow

The dotnet test Android workflow was missing several setup steps that the
working TCP workflow has:
- Java 21 installation (required by Android SDK/emulator)
- mkdir for AVD directory (ANDROID_AVD_HOME must exist)
- ANDROID_EMULATOR_HOME env var
- AndroidSdkDirectory MSBuild property for build

These omissions caused the emulator to not boot or the build to not find
the Android SDK, resulting in the job hanging indefinitely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: align test coverage across MAUI and Blazor, add Credits page

Tests:
- Align BrowserTests with DeviceTests patterns (UnitTests, UnitTestsWithOutput,
  conditional failing, long-running async)
- Add MemberData and IDisposable tests to DeviceTests (from BrowserTests)
- Both now have equivalent test coverage
- Add INCLUDE_FAILING_TESTS conditional compilation to BrowserTests

Credits:
- Add CreditsPage.razor to Blazor visual runner
- Register CreditsViewModel in DI
- Add Credits nav link to TestRunnerApp

UI:
- Fix scroll: nav pinned, main content scrolls

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: scroll lists not pages, remove root back buttons, add ViewModel binding tests

UI:
- Assembly list, test list, diagnostics log, credits content scroll independently
- Nav bar and page headers (Run All, search, filters) stay pinned
- Remove back buttons from Home, Diagnostics, Credits (they're root nav tabs)

Tests:
- Add ViewModelBindingTests: CanLogin, PropertyChanged, ICommand, ClassData
  theory — Blazor equivalent of MAUI's TestPageUITests/DynamicUITests
- Same tests added to both DeviceTests (MAUI) and BrowserTests (Blazor)
- Text transform and ClassData tests mirror MAUI's DynamicUITests patterns

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: replace ClassData with MemberData to avoid serialization error

ClassData with complex record types fails in xunit's reflection-based
discoverer: 'Non-serializable data found'. Use MemberData with primitive
parameters instead. Applied to both BrowserTests and DeviceTests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: support ClassData by setting PreEnumerateTheories=false

The reflection-based discoverer was serializing theory data at discovery
time (PreEnumerateTheories=true), which fails for non-serializable types
like custom records. Setting false defers enumeration to execution time,
matching XunitFrontController behavior.

Restored ClassData tests alongside MemberData — both theory types now
have coverage in both MAUI DeviceTests and Blazor BrowserTests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: create BlazorLibrary with CounterComponent and test parity

New projects:
- DeviceTestingKitApp.BlazorLibrary: Razor Class Library equivalent
  of MauiLibrary — CounterComponent.razor (Blazor equivalent of
  CounterView.xaml) and CounterValueFormatter (equivalent of
  CounterValueConverter)
- DeviceTestingKitApp.BlazorLibrary.XunitTests: tests mirroring
  MauiLibrary.XunitTests — CounterValueFormatter tests for 0/1/N
  formatting plus synthetic pass/skip/fail

Updates:
- MauiLibrary.XunitTests: add REAL CounterValueConverter tests
  (was only synthetic Assert.True before)
- BrowserTests: references BlazorLibrary.XunitTests assembly
  (3 assemblies now discovered)

Test parity:
| Test | MauiLibrary.XunitTests | BlazorLibrary.XunitTests |
|------|----------------------|------------------------|
| Convert/Format 0→Click me! | ✅ | ✅ |
| Convert/Format 1→singular | ✅ | ✅ |
| Convert/Format N→plural | ✅ | ✅ |
| SuccessfulTest | ✅ | ✅ |
| SkippedTest | ✅ | ✅ |
| FailingTest (conditional) | ✅ | ✅ |

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: one class per file, delete Library.XunitTests

- Split multi-class files into separate files matching MAUI convention
- Delete DeviceTestingKitApp.Library.XunitTests — CounterViewModel tests
  moved to BlazorLibrary.XunitTests
- BrowserTests now has 2 test assemblies (own + BlazorLibrary) matching
  MAUI's 2 (own + MauiLibrary)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: restore lost MauiLibrary test files, add Blazor equivalents

Restored from base branch (lost during merges):
- MauiLibrary.XunitTests/Controls/CounterViewTests.cs
- MauiLibrary.XunitTests/Controls/VisualElementTests.cs
- MauiLibrary.XunitTests/Converters/CounterValueConverterTests.cs
- MauiLibrary.XunitTests/Features/MauiSemanticAnnouncerTests.cs

Deleted duplicate CounterValueConverterTests.cs we incorrectly added.

Added Blazor equivalents:
- BlazorLibrary.XunitTests/Formatters/ (mirrors Converters/)
- BlazorLibrary.XunitTests/Controls/ (CounterViewModelTests)
- BlazorLibrary.XunitTests/Features/ (ConsoleSemanticAnnouncerTests
  mirrors MauiSemanticAnnouncerTests — tests the same ISemanticAnnouncer
  pattern with a delegating wrapper instead of MAUI's ISemanticScreenReader)

Folder structure now mirrors MAUI:
  MAUI: Controls/ Converters/ Features/ UnitTests.cs
  Blazor: Controls/ Formatters/ Features/ UnitTests.cs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: exclude trim analysis warnings from TreatWarningsAsErrors

PR #124 enabled TreatWarningsAsErrors globally. The CLI project has
known trim warnings (IL2026, IL2090) from reflection-based JSON/XML
serialization and Spectre.Console.Cli. These are suppressed during
publish via SuppressTrimAnalysisWarnings but TreatWarningsAsErrors
promoted them to errors during normal builds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: use JSON source gen and suppress XML trim warnings properly

Replace reflection-based JsonSerializer calls with source-generated
CliJsonContext for all CommandResult types and Dictionary<string,object>.
This eliminates IL2026 warnings for JSON serialization and removes the
need for JsonSerializerIsReflectionEnabledByDefault.

XML serialization (XmlSerializer) has no source-gen equivalent, so
those call sites use #pragma to suppress IL2026.

Reflection-based text output (GetProperties) uses
DynamicallyAccessedMembers annotations to satisfy IL2090.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: restore dropped comments, remove dead fallback code

Restored comments that were accidentally removed during the source-gen
refactor. Removed the dead JSON fallback path since all CommandResult
types are registered in CliJsonContext.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: implement all test parity gaps from Opus review

IAsyncLifetime + [Collection]:
- Add AsyncLifetimeTests to both BrowserTests and DeviceTests
- Exercises async setup/teardown and test serialization

Component rendering:
- Add CounterComponentTests using HtmlRenderer to render
  CounterComponent.razor and assert HTML output
- Mirrors MAUI's CounterViewTests (initial state, command update)

Production BlazorSemanticAnnouncer:
- Add BlazorSemanticAnnouncer to BlazorLibrary (uses Action<string>
  for JS interop / console fallback)
- Tests now target real production type, not inline stubs

ComponentTestBase fixture:
- Add ComponentTestBase with ctor/Dispose service provider pattern
- Mirrors MAUI's VisualElementTests (verifies clean state)

Hygiene:
- Rename ComponentTests.cs → ViewModelBindingTests.cs
- Restore LongRunningSuccess delay to 2000ms
- Add negative/edge case to CounterValueFormatterTests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: move AsyncLifetimeTests out of UITests, use AsyncLifecycle collection

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor: remove static from ComponentTestBase

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: suppress NU5128 for targets-only NuGet package

DeviceRunners.Testing.Targets has IncludeBuildOutput=false because it
ships only build/ and tools/ content (no lib/ assemblies). NU5128 warns
about the mismatch, which TreatWarningsAsErrors promotes to an error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: close test parity gaps A, B, G, H

Gap A — Button click event dispatch:
- Add bunit to BlazorLibrary.XunitTests
- CounterComponentClickTests dispatches onclick through the rendered
  component (mirrors MAUI SendClicked)

Gap B — LoginComponent rendered tests:
- Create LoginViewModel in BlazorLibrary (mirrors MAUI TestViewModel)
- Create LoginComponent.razor (mirrors MAUI TestPage.xaml)
- LoginComponentTests checks disabled attribute after VM property
  changes (mirrors MAUI TestPageUITests)
- Remove duplicated LoginViewModel from BrowserTests; reference shared

Gap G — MAUI null-guard:
- Add ArgumentNullException guard to MauiSemanticAnnouncer ctor
- Add NullScreenReaderThrows test

Gap H — MAUI converter edge cases:
- Add -1 and int.MaxValue rows to CounterValueConverterTests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address PR review findings

- Quote CLI tool path in Exec command to handle spaces in NuGet cache
  paths (e.g. user profiles with spaces)
- Use AppPort/AppHostNames settings in Windows MSIX launch args instead
  of hardcoding Port and localhost, matching GetAppEnvironmentVariables
- Remove TestingMode=NonInterac…
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 2, 2026
@github-actions github-actions Bot modified the milestones: .NET 10 SR7, .NET 10 SR9 Jul 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

community ✨ Community Contribution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NullReferenceException on iOS in Button.LayoutButton from WrapperView.LayoutSubviews

3 participants