Skip to content

AOT Support for Spectre.Console#1690

Merged
patriksvensson merged 10 commits into
spectreconsole:mainfrom
phil-scott-78:aot-yet-again
Nov 22, 2024
Merged

AOT Support for Spectre.Console#1690
patriksvensson merged 10 commits into
spectreconsole:mainfrom
phil-scott-78:aot-yet-again

Conversation

@phil-scott-78
Copy link
Copy Markdown
Contributor

@phil-scott-78 phil-scott-78 commented Nov 20, 2024

Made another run at AOT support. Spectre.Console itself mostly worked already, but with a few rough spots and unexpected behaviors for edge cases. The biggest issue may have been if someone was using prompts and relying on TypeConverters. @agocke did some work to use the intrinsic ones that I've included here. ExceptionHelper is marked explicitly as not working, as is the entirety of Spectre.Console.Cli.

  • Add AOT compatibility and PolySharp package support
  • Redo TypeConverterHelper based on the work by @agocke to support trimming and AOT
  • Refactor enum value retrieval to use EnumUtils for better compatibility with NetStandard 2.0 and AOT
  • Add RequiresDynamicCode attribute to exception formatter to indicate incompatibility with AOT. Adds a fallback if someone tries to use it.
  • Fix assembly name retrieval for FSharp.Core in TypeNameHelper to work better in AOT scenarios
  • Explicitly marks Spectre.Console.Cli as not trimmable and not appropriate for AOT scenarios. Additionally adds a warning to CommandApp for users who may try it.

Outstanding work would be

  • Documentation of AOT support, and the flag to revert to previous TypeHelper behavior
  • Pick apart how .NET itself handles ToString() these days in AOT and try to mimic that behavior

This replaces the previous PR #1508 and closes #1332 and #1401.

I feel this is pretty solid, and ready for review and merge. I know @Armando-CodeCafe, @BlazeFace, @antoinebj and @TheExiledCat all have express interest recently in the feature so their input would also be appreciated.


Please upvote 👍 this pull request if you are interested in it.

@Armando-CodeCafe
Copy link
Copy Markdown

This is already a great start and should already help in alot of use cases.

What im mostly surprised about is that Spectre.Console is apparently easier to get Native AOT on than Spectre.Console.Cli (im assuming thats due to reflection in the background?)

Considering most users of Spectre.Console also use the cli for their projects i would consider that the next big target. Maybe using source generators instead of reflection (again, if thats the case)

@Armando-CodeCafe
Copy link
Copy Markdown

Made another run at AOT support. Spectre.Console itself mostly worked already, but with a few rough spots and unexpected behaviors for edge cases. The biggest issue may have been if someone was using prompts and relying on TypeConverters. @agocke did some work to use the intrinsic ones that I've included here. ExceptionHelper is marked explicitly as not working, as is the entirety of Spectre.Console.Cli.

  • Add AOT compatibility and PolySharp package support
  • Redo TypeConverterHelper based on the work by @agocke to support trimming and AOT
  • Refactor enum value retrieval to use EnumUtils for better compatibility with NetStandard 2.0 and AOT
  • Add RequiresDynamicCode attribute to exception formatter to indicate incompatibility with AOT. Adds a fallback if someone tries to use it.
  • Fix assembly name retrieval for FSharp.Core in TypeNameHelper to work better in AOT scenarios
  • Explicitly marks Spectre.Console.Cli as not trimmable and not appropriate for AOT scenarios. Additionally adds a warning to CommandApp for users who may try it.

Outstanding work would be

  • Documentation of AOT support, and the flag to revert to previous TypeHelper behavior
  • Pick apart how .NET itself handles ToString() these days in AOT and try to mimic that behavior

This replaces the previous PR #1508 and closes #1332 and #1401.

I feel this is pretty solid, and ready for review and merge. I know @Armando-CodeCafe, @BlazeFace, @antoinebj and @TheExiledCat all have express interest recently in the feature so their input would also be appreciated.

When this is merged i will try it out on my projects that i was getting errors on and i will come back with my results here

@GOATS2K
Copy link
Copy Markdown

GOATS2K commented Nov 20, 2024

Very excited to try this out!

@patriksvensson
Copy link
Copy Markdown
Contributor

Very excited to merge this! 😁

@agocke
Copy link
Copy Markdown

agocke commented Nov 21, 2024

Made another run at AOT support. Spectre.Console itself mostly worked already, but with a few rough spots and unexpected behaviors for edge cases. The biggest issue may have been if someone was using prompts and relying on TypeConverters. @agocke did some work to use the intrinsic ones that I've included here. ExceptionHelper is marked explicitly as not working, as is the entirety of Spectre.Console.Cli.

I agree with this general approach. I tried really hard to get Spectre.Console.Cli to work with a source generator, but ran into too many problems that directly affected the surface area. The dependency injection approach is already really hard to make work without reflection. But even stuff like this:

app.Configure(config =>
{
    config.AddCommand<AddCommand>("add");
    config.AddCommand<CommitCommand>("commit");
    config.AddCommand<RebaseCommand>("rebase");
})

The TCommand parameter there contains properties that are supposed to be the inputs for the command parsing, but ICommand interface doesn't specify anything about them. That basically forces reflection to be used to light up the inputs.

By the time I got something kind-of working the API was just too different. It felt like a reboot. And at that point I figured I might as well try to reuse some code.

My eventual solution was to build a "CLI" format for my serde system: https://github.com/dn-vm/dnvm/tree/main/src/Serde.CmdLine. Serde is a framework for expressing serializers and deserializers and uses a source generator. The model is intended to allow swapping in different formats. CLI parsing is, in some sense, a serialization/deserialization format. So I built a format for that.

It's still very simplistic, but after I round off the edges I might create a NuGet package for it.

@TheExiledCat
Copy link
Copy Markdown

Made another run at AOT support. Spectre.Console itself mostly worked already, but with a few rough spots and unexpected behaviors for edge cases. The biggest issue may have been if someone was using prompts and relying on TypeConverters. @agocke did some work to use the intrinsic ones that I've included here. ExceptionHelper is marked explicitly as not working, as is the entirety of Spectre.Console.Cli.

I agree with this general approach. I tried really hard to get Spectre.Console.Cli to work with a source generator, but ran into too many problems that directly affected the surface area. The dependency injection approach is already really hard to make work without reflection. But even stuff like this:

app.Configure(config =>
{
    config.AddCommand<AddCommand>("add");
    config.AddCommand<CommitCommand>("commit");
    config.AddCommand<RebaseCommand>("rebase");
})

The TCommand parameter there contains properties that are supposed to be the inputs for the command parsing, but ICommand interface doesn't specify anything about them. That basically forces reflection to be used to light up the inputs.

By the time I got something kind-of working the API was just too different. It felt like a reboot. And at that point I figured I might as well try to reuse some code.

My eventual solution was to build a "CLI" format for my serde system: https://github.com/dn-vm/dnvm/tree/main/src/Serde.CmdLine. Serde is a framework for expressing serializers and deserializers and uses a source generator. The model is intended to allow swapping in different formats. CLI parsing is, in some sense, a serialization/deserialization format. So I built a format for that.

It's still very simplistic, but after I round off the edges I might create a NuGet package for it.

This would be insanely cool if it would be able to work, however i think there would be some kind of way to do the thing in your example using for example roslyn source gens by grabbing the attributes of the Command specifying the options and settings and then the roslyn source gen would have to add code to the build manually setting the different options to their
Lets say the settings have a property called Port "--port "

The roslyn source gen would have to generate some kind of line and add it to the source like:

`Port = args.ParseThePortAndDoTheThings("--port ")

So i dont think we would need a external lib for this, using some well defined roslyn generators these commands and settings can be set using some kind of functions injected at build time that contain all of the different properties and their respective cli strings

@phil-scott-78
Copy link
Copy Markdown
Contributor Author

phil-scott-78 commented Nov 21, 2024

I think at the end of the day to do AOT in Spectre.Console.Cli would either take considerable effort or a change in the API. Between DI, TypeConverters, MultiMap<>, dynamically creating arrays and even some magic in there for automatically instantiating things like DirectoryInfo properties, there are just so many edge cases that are deep within the codebase that we'd be in a situation where we'd never know if any change was going to break AOT and/or we'd be in the weeds managing System.Diagnostics.CodeAnalysis attributes with every new update.

Additionally, a while back, we split the Cli and Console app with the knowledge there were more "modern" libraries like System.CommandLine that integrate well with Spectre.Console that having the benefit of being developed after the introduction of things like assembly trimming have support designed in.

I do think a third party that wanted to generate a source generator that scans for Settings and dumps a ton of DynamicDependency attributes for the settings and their properties might get people limping along, but I'm not sure I'd ever feel comfortable including it in the official project. I certainly wouldn't love asking the other maintainers to have to own it either.

@TheExiledCat
Copy link
Copy Markdown

I think at the end of the day to do AOT in Spectre.Console.Cli would either take considerable effort or a change in the API. Between DI, TypeConverters, MultiMap<>, dynamically creating arrays and even some magic in there for automatically instantiating things like DirectoryInfo properties, there are just so many edge cases that are deep within the codebase that we'd be in a situation where we'd never know if any change was going to break AOT and/or we'd be in the weeds managing System.Diagnostics.CodeAnalysis attributes with every new update.

Additionally, a while back, we split the Cli and Console app with the knowledge there were more "modern" libraries like System.CommandLine that integrate well with Spectre.Console that having the benefit of being developed after the introduction of things like assembly trimming have support designed in.

I do think a third party that wanted to generate a source generator that scans for Settings and dumps a ton of DynamicDependency attributes for the settings and their properties might get people limping along, but I'm not sure I'd ever feel comfortable including it in the official project. I certainly wouldn't love asking the other maintainers to have to own it either.

Thats fair, so it seems this one is just gonna be: who wants to either put an unrealistic amount of effort to change the current Codebase or just rewrite it from scratch to native aot, so the options arent great here i guess.

@patriksvensson
Copy link
Copy Markdown
Contributor

patriksvensson commented Nov 21, 2024

Once AOT for Spectre.Console is in, I will take a look at generating the command tree using source generators instead of doing it by reflection. I have some ideas.

@phil-scott-78
Copy link
Copy Markdown
Contributor Author

Redid the ExceptionFormatter code. The more I thought about it, it's not like the default .NET behavior is all that great anyways. So now it still uses the nicer format for printing the exception and message, then falls back to the StackFrame ToString() method to get as much info as we can at runtime.

While I was in there I went ahead and added support for Spectre.Console.Json and Spectre.Console.ImageSharp, and I even added a blurb in the documentation's best practice section.

@agocke
Copy link
Copy Markdown

agocke commented Nov 22, 2024

FYI I’m totally willing to help anyone who wants to dive in the rabbit hole of writing a source generator. I just hit a wall and happened to have an alternative that I could use to unblock my app. FYI I’ve been using the rest of spectre console in dnvm as aot and it has worked great.

@phil-scott-78
Copy link
Copy Markdown
Contributor Author

I have nothing really to add here anymore. Pending a review, I say ready for merge.

@patriksvensson
Copy link
Copy Markdown
Contributor

@phil-scott-78 I will be taking a look now. Not sure I will understand everything though 😁

@phil-scott-78
Copy link
Copy Markdown
Contributor Author

Anything needing clarification or more comments to describe what's happening, I'm happy to expand!

Copy link
Copy Markdown
Contributor

@patriksvensson patriksvensson left a comment

Choose a reason for hiding this comment

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

It looks good to me. A question, though: Can we remove reliance on the other polyfill packages now that PolySharp has been introduced?

Copy link
Copy Markdown
Contributor

@patriksvensson patriksvensson left a comment

Choose a reason for hiding this comment

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

It looks good to me. A question, though: Can we remove reliance on the other polyfill packages now that PolySharp has been introduced?

mattleibow added a commit to mattleibow/DeviceRunners that referenced this pull request May 8, 2026
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>
mattleibow added a commit to mattleibow/DeviceRunners that referenced this pull request May 11, 2026
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>
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…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AOT Support

8 participants