Add MSTest.Windows.AppTesting thin-layer for Windows desktop E2E testing#7810
Add MSTest.Windows.AppTesting thin-layer for Windows desktop E2E testing#7810Evangelink wants to merge 13 commits into
Conversation
Introduce a new MSTest.DesktopTesting package that provides base classes (AutomationTest, ApplicationTest, WindowTest) for end-to-end testing of Windows desktop applications. Follows the same pattern as Playwright MSTest integration: a thin lifecycle adapter over System.Windows.Automation. - AutomationTest: hierarchy root (extensible) - ApplicationTest: process launch/teardown via Process.Start - WindowTest: exposes MainWindow as AutomationElement SDK integration: - EnableDesktopTesting MSBuild property in MSTest.Sdk - DesktopTesting.targets feature file (ClassicEngine, VSTest, NativeAOT guard) - Version wired through Sdk.props.template Also includes: - Sample project (ProjectUsingDesktopTesting with Calculator tests) - Acceptance tests for both MTP runner and VSTest modes
There was a problem hiding this comment.
Pull request overview
Adds a new Windows-desktop E2E testing thin layer (MSTest.DesktopTesting) and wires it into MSTest.Sdk behind an <EnableDesktopTesting> feature flag, plus samples and acceptance coverage.
Changes:
- Introduces
Microsoft.MSTest.DesktopTestingbase classes (AutomationTest→ApplicationTest→WindowTest) built onSystem.Windows.Automation. - Adds MSTest.Sdk feature wiring (
DesktopTesting.targetsimport + version plumbing + NativeAOT guard). - Adds a public sample and an acceptance test that exercises the SDK flag in both MSTest runner and VSTest modes.
Show a summary per file
| File | Description |
|---|---|
| test/IntegrationTests/MSTest.Acceptance.IntegrationTests/DesktopTestingSdkTests.cs | Adds acceptance tests that generate a project enabling the DesktopTesting SDK feature. |
| src/TestFramework/TestFramework.DesktopTesting/AutomationTest.cs | Adds hierarchy root base class for desktop automation tests. |
| src/TestFramework/TestFramework.DesktopTesting/ApplicationTest.cs | Adds process lifecycle management for desktop app E2E tests. |
| src/TestFramework/TestFramework.DesktopTesting/WindowTest.cs | Exposes main window as AutomationElement for tests. |
| src/TestFramework/TestFramework.DesktopTesting/GlobalUsings.cs | Adds MSTest global using for the new package. |
| src/TestFramework/TestFramework.DesktopTesting/TestFramework.DesktopTesting.csproj | Defines the new packable Windows-desktop testing project/package. |
| src/TestFramework/TestFramework.DesktopTesting/PublicAPI/PublicAPI.Shipped.txt | Initializes shipped API baseline for the new package. |
| src/TestFramework/TestFramework.DesktopTesting/PublicAPI/PublicAPI.Unshipped.txt | Declares new public API surface for DesktopTesting. |
| src/Package/MSTest.Sdk/Sdk/Features/DesktopTesting.targets | Implements the SDK feature target that injects the DesktopTesting package + implicit using. |
| src/Package/MSTest.Sdk/Sdk/VSTest/VSTest.targets | Imports DesktopTesting feature targets in VSTest mode. |
| src/Package/MSTest.Sdk/Sdk/Runner/ClassicEngine.targets | Imports DesktopTesting feature targets in ClassicEngine mode. |
| src/Package/MSTest.Sdk/Sdk/Runner/NativeAOT.targets | Adds a NativeAOT validation error for the feature flag. |
| src/Package/MSTest.Sdk/Sdk/Sdk.props.template | Adds EnableDesktopTesting default + version property. |
| src/Package/MSTest.Sdk/MSTest.Sdk.csproj | Plumbs DesktopTesting version into SDK props templating. |
| samples/public/DemoMSTestSdk/ProjectUsingDesktopTesting/ProjectUsingDesktopTesting.csproj | Adds sample project demonstrating the SDK flag. |
| samples/public/DemoMSTestSdk/ProjectUsingDesktopTesting/CalculatorTests.cs | Adds sample Calculator tests using WindowTest + UIA. |
| Directory.Packages.props | Introduces MSTestDesktopTestingVersion and a declared PackageVersion entry. |
Copilot's findings
Comments suppressed due to low confidence (1)
samples/public/DemoMSTestSdk/ProjectUsingDesktopTesting/CalculatorTests.cs:45
- The sample locates buttons by
AutomationElement.NamePropertywith values "One"/"Two". These names are localized in many Windows locales, so the sample can be flaky or fail outside English. Prefer a locale-independent locator (e.g., AutomationId) or note the limitation explicitly in comments.
AutomationElement? button1 = MainWindow.FindFirst(
TreeScope.Descendants,
new PropertyCondition(AutomationElement.NameProperty, "One"));
AutomationElement? button2 = MainWindow.FindFirst(
TreeScope.Descendants,
new PropertyCondition(AutomationElement.NameProperty, "Two"));
- Files reviewed: 17/17 changed files
- Comments generated: 5
- Replace busy-wait (Thread.Yield) with Thread.Sleep(50ms) polling in ApplicationSetup - Make ApplicationTearDown race-safe with try/finally and InvalidOperationException handling - Always set Windows TFMs with EnableWindowsTargeting for non-Windows CI builds - Add MSBuild error in DesktopTesting.targets when targeting non-Windows TFM - Make sample CalculatorTests locale-agnostic using AutomationId instead of localized names
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Test Expert Reviewer 🧪
Date: 2026-05-11
Repository: microsoft/testfx
Key Findings
-
[Coverage] MTP runner test has no test-count assertion (
WindowsAppTestingSdkTests.cs:80) — OnlyAssertExitCodeIsis called. If 0 tests ran, this silently passes. The VSTest counterpart correctly asserts"Passed: 2".AspireSdkTestsandPlaywrightSdkTestsboth useAssertOutputContainsSummaryfor the MTP case. -
[Structure]
NopAssetFixture+ per-testGenerateAssetAsyncdiverges from repo pattern (WindowsAppTestingSdkTests.cs:11) — Every test independently compiles the test asset from source. The repo'sAspireSdkTests/PlaywrightSdkTestsuse an innerTestAssetFixture : TestAssetFixtureBasethat generates the asset once inClassInitialize, then both tests locate and run against the pre-built asset. -
[Flakiness]
CalculatorTests.csautomation IDs are broken on modern Windows (CalculatorTests.cs:47) —calc.exeon Windows 10/11 is a UWP app whose element tree does not containnum1Button/num2Button. The sample test will always fail as written on any modern Windows installation, which undermines its value as reference documentation.
Recommendations
- Add
AssertOutputContainsSummary(0, 2, 0)(or equivalent) after theAssertExitCodeIsin the MTP runner test. - Refactor
WindowsAppTestingSdkTeststo use theTestAssetFixturepattern to match Aspire/Playwright tests and avoid redundant per-test compilation. - Replace the broken
Calculator_NumberButtons_AreVisibletest in the sample with either a stable Win32 app (e.g.,notepad.exe) or add a clear comment that the automation IDs are app-version-specific and must be adapted.
Generated by Test Expert Reviewer 🧪
🧪 Test quality reviewed by Test Expert Reviewer 🧪
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Expert Code Reviewer
Date: 2026-05-11
Repository: microsoft/testfx
Key Findings
🔴 Correctness — Critical
[TestClass] on WindowTest silently drops STA threading for the entire hierarchy (WindowTest.cs line 42)
STATestClassAttribute is declared with [AttributeUsage(Inherited = false)]. Because of this:
GetCustomAttributes(inherit: true)onWindowTestdoes not return[STATestClass]fromApplicationTest- MSTest resolves
ClassAttributeas a plainTestClassAttribute, soClassAttribute is STATestClassAttributeisfalse GetTestMethodAttributereturns a regularTestMethodAttribute— tests run on MTA, not STA
The same logic applies to every user class that writes [TestClass] class MyTests : WindowTest. All of them will run on MTA, causing Windows UI Automation COM interop to fail with InvalidOperationException or COMException (0x80010106) at runtime.
Fix needed: Change [TestClass] → [STATestClass] on WindowTest (same for ApplicationTest is already correct). Document clearly that user test classes must also use [STATestClass]. Longer term, consider changing STATestClassAttribute to Inherited = true so the constraint propagates automatically.
🟡 Defensive — Important
AutomationElement.FromHandle can return null (WindowTest.cs line 58)
If the window handle becomes invalid between ApplicationSetup and WindowSetup, FromHandle returns null, which is silently stored in the non-nullable MainWindow property. Tests then crash with an uninformative NullReferenceException. A null check with a descriptive InvalidOperationException is needed.
🟡 Resources — Minor
Process.Kill() without entireProcessTree: true (ApplicationTest.cs line 107)
Desktop applications often spawn child processes. Killing only the top-level process leaves children alive (handles, file locks), which can cause subsequent tests to fail. Use Kill(entireProcessTree: true) — available on both net8.0-windows and net9.0-windows.
🟡 Correctness — Minor
Early process exit not detected in setup polling loop (ApplicationTest.cs line 69)
If the app crashes at startup, the loop spins for the full ApplicationStartTimeout and then throws a misleading TimeoutException. Adding an AppProcess.HasExited check inside the loop and surfacing the exit code gives developers actionable diagnostics.
Positive Observations
- The thin-layer pattern (mirroring Playwright/Aspire) is a clean and familiar design for MSTest users.
- The teardown
try/finallywithInvalidOperationExceptionhandling correctly handles the race betweenHasExitedchecks and process operations. - SDK integration (ClassicEngine, VSTest, NativeAOT guard,
Sdk.props.template) is done consistently with the Playwright/Aspire pattern. EnableWindowsTargetingfor non-Windows CI builds is a thoughtful touch.PublicAPI.Unshipped.txtis properly maintained.
Generated by Expert Code Reviewer
🧠 Reviewed by Expert Code Reviewer 🧠
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Expert Code Reviewer 🧠
Date: 2026-05-12
Repository: microsoft/testfx
Key Findings
All four issues flagged in the previous expert review (run 25696190614) have been fully addressed in the latest commits:
| Previous Finding | Status |
|---|---|
[TestClass] on WindowTest silently drops STA threading for the entire hierarchy |
✅ Fixed — [STATestClass] applied; docs and samples updated |
AutomationElement.FromHandle null return not handled |
✅ Fixed — null-coalescing throw with descriptive message added |
Process.Kill() without entireProcessTree: true |
✅ Fixed — child processes now terminated on force-kill |
| Early process exit not detected in polling loop | ✅ Fixed — HasExited check with exit code in error message |
Additionally, both Test Expert findings have been addressed: AssertOutputContainsSummary is now called in the MTP runner test, and the TestAssetFixture pattern has been adopted to match AspireSdkTests/PlaywrightSdkTests.
Positive Observations
- Correctness:
ApplicationSetup/WindowSetup[TestInitialize]ordering is correct — MSTest calls base-class initializers first, so the process is guaranteed to be running before theAutomationElementis obtained. - Teardown:
try/finallywithInvalidOperationExceptionguard correctly handles the race betweenHasExitedand process operations;WaitForExit(5000)afterKillavoids zombie handles. - SDK integration:
WindowsAppTesting.targetscorrectly validates the Windows TFM at build time and is guarded from NativeAOT; theSdk.props.templateversioning aligns with the Playwright/Aspire pattern. - Acceptance test:
NotepadTestsuses a stable Win32 app and validates both a non-nullMainWindowand a non-empty title — robust against Notepad UI changes.
No New Issues
No correctness, threading, performance, API compatibility, resource management, or security issues were found in the updated code.
Generated by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Test Expert Reviewer 🧪
Date: 2026-05-12
Repository: microsoft/testfx
Key Findings
The PR has been significantly improved since the previous review (the AssertOutputContainsSummary gap and TestAssetFixture pattern have both been addressed ✅). Three issues remain:
-
[Assertion]
Notepad_MainWindow_IsNotNullis a vacuous assertion (WindowsAppTestingSdkTests.csembedded NotepadTests, line 79) —WindowSetupalready guaranteesMainWindowis non-null (throws if not). The assertion can never be the actual cause of a test failure; it gives false confidence of coverage. -
[Flakiness]
Calculator_NumberButtons_AreVisiblealways fails on modern Windows (CalculatorTests.cslines 44-59) — The UWP Calculator (calc.exeon Windows 10/11) does not exposenum1Button/num2Buttonin its automation tree. The class-level XML doc notes this, but a broken sample test is more confusing than helpful. Usingnotepad.exe(already stable in the acceptance tests) or adding[Ignore]would prevent users from running into an immediate, unexplained failure. -
[Coverage] Single build artifact used for both MTP and VSTest acceptance tests (
WindowsAppTestingSdkTests.csline 25) — Both tests share an asset configured withrunner: VSTestinglobal.json. The MTP runner path (TestHost.ExecuteAsync) works because it bypassesdotnet test, but no acceptance test covers a default/MTP-native build, so an MSTest.Sdk regression in that mode would go undetected.
Recommendations
- Replace the vacuous
Assert.IsNotNull(MainWindow)with a meaningful assertion (e.g.,Assert.AreEqual(ControlType.Window, MainWindow.Current.ControlType)), or remove the redundant test and rely on the title test alone. - Replace
calc.exein the sample withnotepad.exe(or apply[Ignore]to the broken test) so the shipped sample is immediately runnable. - Add a second test asset variant (or parameterise the existing one) that builds without
runner: VSTestto cover the default MTP runner path end-to-end.
🧪 Test quality reviewed by Test Expert Reviewer 🧪
🧪 Test quality reviewed by Test Expert Reviewer 🧪
- Add WindowsAppTesting.targets to MSTest.Sdk.nuspec (fixes MSB4019 build error) - Handle empty/whitespace DESKTOP_TEST_APP_PATH env var in ApplicationPath - Fix misleading 'handle' wording in WindowTest remarks to say AutomationElement - Guard WindowsAppTesting.targets validation for cross-targeting outer builds - Add [Ignore] to Calculator_NumberButtons_AreVisible sample (broken on UWP Calculator) - Replace vacuous Assert.IsNotNull(MainWindow) with ControlType.Window assertion
…om/microsoft/testfx into dev/amauryleve/desktop-testing-e2e
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Test Expert Reviewer 🧪
Date: 2026-05-12
Repository: microsoft/testfx
Key Findings
The PR has addressed all issues from the two previous test reviews ✅:
AssertOutputContainsSummary(0, 2, 0)is now present in the MTP runner test- Vacuous
Assert.IsNotNull(MainWindow)was replaced with the meaningfulControlType.Windowassertion inNotepadTests Calculator_NumberButtons_AreVisiblenow carries[Ignore]with an explanatory message
Two minor issues remain:
-
[Assertion]
EnableWindowsAppTesting_WhenUsingVSTest_RunsDesktopTestsdoes not assert"VSTest version"(WindowsAppTestingSdkTests.csline 42) — BothAspireSdkTestsandPlaywrightSdkTestsincludedotnetTestResult.AssertOutputContains("VSTest version")to confirm VSTest was actually invoked. Without it, the test passes even if the run silently fell back to the MTP runner, making theWhenUsingVSTestclaim unverifiable. -
[Assertion]
Assert.IsNotNull(MainWindow)in the sampleCalculatorTests.csis vacuous (line 35) —WindowSetup()already throws before the test body ifMainWindowcannot be obtained, so this assertion can never catch a real failure. The same fix applied to the embeddedNotepadTests(usingAssert.AreEqual(ControlType.Window, ...)) should be applied here for consistency.
Recommendations
- Add
dotnetTestResult.AssertOutputContains("VSTest version");inEnableWindowsAppTesting_WhenUsingVSTest_RunsDesktopTests. - Replace
Assert.IsNotNull(MainWindow)inCalculatorTests.Calculator_MainWindow_HasExpectedTitlewithAssert.AreEqual(ControlType.Window, MainWindow.Current.ControlType, ...), consistent with the acceptance test.
🧪 Test quality reviewed by Test Expert Reviewer 🧪
🧪 Test quality reviewed by Test Expert Reviewer 🧪
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Expert Code Reviewer 🧠
Date: 2026-05-12
Repository: microsoft/testfx
Key Findings
The four issues from the first expert review (run 25696190614) are all correctly addressed in the current HEAD. Three new minor observations remain:
🟡 Minor — Correctness
AppProcess.Refresh() missing before final timeout check (ApplicationTest.cs:93)
The cached MainWindowHandle value is up to one poll interval (≤50 ms) stale when the while loop exits due to timeout. A final Refresh() before the post-loop check prevents a false TimeoutException at the boundary.
🟡 Minor — Resource management
Win32Exception not caught in ApplicationTearDown (ApplicationTest.cs:121)
CloseMainWindow() and Kill() are both documented to throw Win32Exception (e.g., different desktop session, insufficient permissions). The current catch (InvalidOperationException) misses these, causing the process to run orphaned when teardown is triggered against an elevated or session-isolated process.
🔵 Informational — Cross-TFM dependency
UseWPF=true forces WPF as transitive dependency (TestFramework.Windows.AppTesting.csproj:6)
NuGet propagates framework references, so all consumers (Win32 or WinForms test projects) silently acquire WPF. The package only needs UIAutomation assemblies, which are available without WPF via <FrameworkReference Include="Microsoft.WindowsDesktop.App" />.
Positive Observations
- Process teardown is robust:
[TestCleanup]runs even when[TestInitialize]throws, thefinallyblock always disposes the .NET handle, andKill(entireProcessTree: true)correctly handles child processes. [STATestClass]placement is correct:ApplicationTestandWindowTestboth carry it;AutomationTestintentionally uses[TestClass]as the extension-point root. STA is inherited by derived user classes.- Startup polling loop correctly calls
Refresh()before eachHasExitedandMainWindowHandlecheck; theThread.Sleep(min(remaining, 50ms))avoids overshooting the timeout significantly. - Null safety in
WindowSetup— the null-coalescing throw onAutomationElement.FromHandlegives an actionable error message. - SDK wiring mirrors the Playwright/Aspire patterns precisely: feature targets, props template, NativeAOT guard, and the build-time TFM error are all consistent with the existing infrastructure.
Generated by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
- Add 'VSTest version' assertion to confirm VSTest runner is used - Replace vacuous Assert.IsNotNull(MainWindow) with ControlType.Window check in sample - Add final AppProcess.Refresh() before post-loop MainWindowHandle check - Widen teardown catch to include Win32Exception for inaccessible processes - Replace UseWPF with FrameworkReference to Microsoft.WindowsDesktop.App - Update MSTest.Sdk expected file count (15 -> 16) for WindowsAppTesting.targets
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Test Expert Reviewer 🧪
Date: 2026-05-12
Repository: microsoft/testfx
Key Findings
-
✅ [Coverage] MTP runner test count assertion — FIXED (
WindowsAppTestingSdkTests.cs:24) — The previously missingAssertOutputContainsSummary(failed: 0, passed: 2, skipped: 0)call is now present. Both the MTP and VSTest tests correctly validate test counts, consistent withAspireSdkTestsandPlaywrightSdkTests. -
[Coverage] Only
NetCurrent-windowsTFM tested (WindowsAppTestingSdkTests.cs:13) —PlaywrightSdkTests(the closest equivalent) uses[DynamicData(nameof(TargetFrameworks.AllForDynamicData))]across all TFMs. The Windows AppTesting tests only exercisenet10.0-windows. If the package supportsnet8.0-windows, regressions on that TFM are invisible to CI. See inline comment for a concrete fix usingTargetFrameworks.Netmapped to Windows TFMs.
Observations
- All 25 review threads from the previous cycle are resolved or outdated — no open items carried over.
- Test isolation is clean:
ApplicationSetup/ApplicationTearDownbracketing is correct. TheThread.Sleepin the polling loop is bounded to ≤50 ms per iteration and protected byApplicationStartTimeout— acceptable for a UI automation framework. - The embedded
NotepadTests.csassertions (ControlType.Window, non-empty title) are adequate for an acceptance test verifying the library plumbing. No false-positive risk. [OSCondition(OperatingSystems.Windows)]correctly guards both test methods, so non-Windows CI will report skips rather than failures.
Recommendations
- Extend TFM coverage to all
TargetFrameworks.Netentries suffixed with-windows(see inline comment on line 13).
Generated by Test Expert Reviewer 🧪
🧪 Test quality reviewed by Test Expert Reviewer 🧪
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Expert Code Reviewer 🧠
Date: 2026-05-12
Repository: microsoft/testfx
Key Findings
The third round of fixes (commit c9ac13f) resolved all previously open correctness issues:
- ✅
AppProcess.Refresh()now called before the post-timeoutMainWindowHandlecheck - ✅
Win32Exceptionnow caught inApplicationTearDownalongsideInvalidOperationException - ✅
UseWPF=truereplaced with an explicitFrameworkReference
🟡 One remaining concern
FrameworkReference Include="Microsoft.WindowsDesktop.App" (csproj line 8) — the switch from UseWPF=true replaced one WPF-only reference with a broader reference that includes both WPF and WinForms. Since System.Windows.Automation resides in the WPF subsystem, Microsoft.WindowsDesktop.App.WPF is the correct minimal reference. See inline comment for details.
✅ What was verified
- Lifecycle ordering:
ApplicationSetup→WindowSetup(base-first init), singleApplicationTearDowncleanup — correct MSTest ordering - Process teardown:
CloseMainWindow→WaitForExit(5s)→Kill(entireProcessTree)→Disposein try/finally — no leaks in any error path Win32Exceptionhandling: correctly catches post-HasExited races in teardown- Post-loop
Refresh()guard: main window handle is freshly validated beforeWindowSetupconsumes it - SDK integration: NativeAOT guard, feature-flag pattern, nuspec wiring, version templating — all correct
- Public API surface:
PublicAPI.Unshipped.txtis complete and accurate
Recommendations
Change <FrameworkReference Include="Microsoft.WindowsDesktop.App" /> to <FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" /> to avoid pulling WinForms assemblies into consumers that don't need them.
Generated by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Expert Code Reviewer 🧠
Date: 2026-05-12
Repository: microsoft/testfx
Key Findings
All open findings from previous expert reviews are resolved in the current HEAD (76e6f0b):
| Previous finding | Status |
|---|---|
AppProcess.Refresh() missing before final timeout check |
✅ Fixed — Refresh() now called unconditionally before the TimeoutException check |
Win32Exception not caught in ApplicationTearDown |
✅ Fixed — catch (Exception ex) when (ex is InvalidOperationException or Win32Exception) |
FrameworkReference overly broad (Microsoft.WindowsDesktop.App) |
✅ Fixed — now scoped to Microsoft.WindowsDesktop.App.WPF, which is the minimum required for System.Windows.Automation |
No new correctness, threading, performance, API-compat, cross-TFM, resource, or security issues were found in this pass.
Positive Observations
- Startup polling loop is correct: each iteration calls
Refresh()andHasExitedbefore sleeping, avoiding a busy-spin; the trailingRefresh()+ handle check after the loop exits is clean. - Teardown is robust:
try/finallyguaranteesDispose()even ifCloseMainWindow()/Kill()throw;Kill(entireProcessTree: true)handles child processes;WaitForExit(5000)post-kill avoids zombie handles. [STATestClass]placement is correct:ApplicationTestandWindowTestcarry it, and sinceInherited = true(the default forAttributeUsageAttribute), all user-derived classes automatically run on the STA thread without needing to redeclare the attribute — whileAutomationTestdeliberately stays[TestClass]as the extension-point root.WindowSetupordering:[TestInitialize]methods run base-to-derived, soApplicationSetup(which setsAppProcess) always precedesWindowSetup(which readsAppProcess.MainWindowHandle). IfApplicationSetupthrows,WindowSetupis skipped by the MSTest runner, so no NRE path exists.- PublicAPI.Unshipped.txt correctly declares the full public surface for the new package.
- SDK integration mirrors the Playwright/Aspire pattern exactly: feature targets,
NativeAOT.targetsguard,Sdk.props.templateversion injection, Central Package Management support.
Overall Assessment
The PR is ready to merge. The implementation is clean, safe, and consistent with the existing thin-layer pattern used by Playwright and Aspire.
Generated by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Test Expert Reviewer 🧪
Date: 2026-05-12
Repository: microsoft/testfx
Key Findings
All issues from previous test reviews have been addressed ✅, and the latest commit (76e6f0b) adds clean multi-TFM coverage:
AssertOutputContainsSummary(0, 2, 0)is present in the MTP runner test ✓AssertOutputContains("VSTest version")confirms VSTest invocation in the VSTest test ✓AssertOutputContains("Passed! - Failed: 0, Passed: 2, ...")verifies test counts in VSTest mode ✓[DynamicData(nameof(DesktopTargetFrameworksForDynamicData))]now runs both tests across all supported Windows TFMs (consistent withPlaywrightSdkTestsapproach for platform-specific TFMs) ✓PatchTargetFrameworks(DesktopTargetFrameworks)correctly builds the asset for all Windows TFMs in a single pass ✓TestAssetFixturepattern matches the repo's Aspire/Playwright convention — asset built once, reused by all test cases ✓- Assertions in the embedded
NotepadTests.csandCalculatorTests.cssample are meaningful (ControlType.Windowcheck, not vacuousIsNotNull) ✓
Recommendations
No further changes needed. Tests are well-isolated, structurally consistent with existing acceptance tests, and cover the expected behavior across all Windows TFMs.
Generated by Test Expert Reviewer 🧪
🧪 Test quality reviewed by Test Expert Reviewer 🧪
🧪 Test quality reviewed by Test Expert Reviewer 🧪
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Test Expert Reviewer 🧪
Date: 2026-05-12
Repository: microsoft/testfx
Key Findings
Both issues flagged in the previous test review (2026-05-12T09:55) have been resolved ✅:
-
[Assertion] VSTest version assertion —
dotnetTestResult.AssertOutputContains("VSTest version")is now present inEnableWindowsAppTesting_WhenUsingVSTest_RunsDesktopTests(line 44), confirming VSTest was actually invoked. -
[Assertion] Vacuous
Assert.IsNotNull(MainWindow)in sample —CalculatorTests.Calculator_MainWindow_HasExpectedTitlenow usesAssert.AreEqual(ControlType.Window, MainWindow.Current.ControlType, ...), matching the meaningful assertion style established in the embeddedNotepadTests.
The test suite is well-structured:
- Both MTP runner and VSTest invocation paths are covered
- Each test asserts exit code and explicit pass/fail/skip counts
OSConditioncorrectly guards Windows-only tests for non-Windows CI[STATestClass]threading requirement is respected throughout[Ignore]with an explanatory message covers the AutomationId-specific sample test
Recommendations
None — all previously flagged issues have been addressed.
🧪 Test quality reviewed by Test Expert Reviewer 🧪
🧪 Test quality reviewed by Test Expert Reviewer 🧪
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Expert Code Reviewer 🧠
Date: 2026-05-12
Repository: microsoft/testfx
Key Findings
All three observations from the previous expert review (run 25732804835) are addressed in commit 76e6f0b:
| Previous Finding | Status |
|---|---|
AppProcess.Refresh() missing before final timeout check (ApplicationTest.cs:93) |
✅ Refresh() call is present at line 94 |
Win32Exception not caught in ApplicationTearDown |
✅ catch (Exception ex) when (ex is InvalidOperationException or Win32Exception) on line 123 |
UseWPF=true forces WPF as transitive dependency |
✅ Narrowed to <FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" /> — correct minimal reference since UIAutomationClient.dll lives in the WPF framework subset |
Fresh Analysis (No New Issues Found)
After reviewing all changed files in the current HEAD (66d35e0):
- Correctness: Lifecycle ordering is correct — MSTest calls base-class
[TestInitialize]first, soAppProcessis always set beforeWindowSetupaccesses it;[TestCleanup]onApplicationTestruns even when[TestInitialize]throws, so process handles are always cleaned up. - Threading:
[STATestClass]is correctly applied to bothApplicationTestandWindowTest; the sample and acceptance test both redeclare[STATestClass]on the user test class (required becauseSTATestClassAttributehasInherited = false). - Resources:
try/finallyinApplicationTearDownguarantees disposal;Kill(entireProcessTree: true)handles child processes;WaitForExit(5000)after kill prevents zombie handles. - API surface:
PublicAPI.Unshipped.txtis complete and correct; all public types have XML docs; noinitaccessors in new public API. - Cross-TFM:
_ValidateWindowsAppTestingPlatformtarget errors at Restore/Build time for non-Windows TFMs;EnableWindowsTargetingguard in the.csprojhandles non-Windows CI builds. - Acceptance tests: Both MTP runner and VSTest paths now assert
AssertOutputContainsSummary(failed: 0, passed: 2, skipped: 0)/"Passed: 2", consistent withAspireSdkTestsandPlaywrightSdkTests;[DynamicData]loops over all desktop TFMs.
Overall Assessment
The implementation is clean, correct, and complete. The thin-layer pattern faithfully mirrors Playwright/Aspire, the teardown logic is race-safe, and all previously identified issues have been resolved. No correctness, threading, performance, API compatibility, resource management, or security concerns remain.
Generated by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
Summary
Introduces a new
MSTest.Windows.AppTestingNuGet package that provides base classes for end-to-end testing of Windows desktop applications (WinForms, WPF, Win32). Follows the same thin-layer pattern asMicrosoft.Playwright.MSTest— a lifecycle adapter over the built-inSystem.Windows.AutomationAPI with zero external dependencies.Base class hierarchy
AutomationTest— root of the hierarchy, extension point for future configurationApplicationTest— launches the app viaProcess.Start, waits for main window handle, manages teardown withCloseMainWindow/Kill. Uses[STATestClass]for COM/STA threading. Configurable via virtual properties (ApplicationPath,ApplicationArguments,ApplicationStartTimeout)WindowTest— exposesMainWindowasSystem.Windows.Automation.AutomationElement. Users can layer FlaUI or other libraries on top for richer element interactionSDK integration
Mirrors the Playwright/Aspire SDK pattern exactly:
<EnableWindowsAppTesting>true</EnableWindowsAppTesting>MSBuild property in MSTest.SdkWindowsAppTesting.targetsfeature file wired into ClassicEngine, VSTest, and NativeAOT targetsSdk.props.template<PackageReference>User experience
What's included
src/TestFramework/TestFramework.Windows.AppTesting/— the package sourcesamples/public/DemoMSTestSdk/ProjectUsingWindowsAppTesting/— Calculator E2E testWindowsAppTestingSdkTests.cs— tests both MTP runner and VSTest modes using notepad.exeDesign decisions
System.Windows.Automationover FlaUI: Zero external dependencies. Uses the managed UIA wrapper from the Windows Desktop runtime. Users who want a richer API can layer FlaUI on top.[STATestClass]: Required for COM interop with Windows UI AutomationEnableWindowsTargeting: Always sets Windows TFMs withEnableWindowsTargetingfor non-Windows CI compatibilityApplicationTearDownuses try/finally withInvalidOperationExceptionhandling for process exit races