Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<!-- Test Platform, .NET Test SDK and Object Model -->
<MicrosoftNETTestSdkVersion>18.4.0</MicrosoftNETTestSdkVersion>
<MicrosoftPlaywrightVersion>1.58.0</MicrosoftPlaywrightVersion>
<MSTestWindowsAppTestingVersion>4.3.0</MSTestWindowsAppTestingVersion>
<MicrosoftTestingExtensionsFakesVersion>18.1.1</MicrosoftTestingExtensionsFakesVersion>
<MicrosoftTestingInternalFrameworkVersion>1.5.0-preview.24577.4</MicrosoftTestingInternalFrameworkVersion>
<SystemThreadingTasksExtensionsVersion>4.5.4</SystemThreadingTasksExtensionsVersion>
Expand Down Expand Up @@ -97,6 +98,7 @@
This allows dependabot to suggest updates.
-->
<ItemGroup Label="Declared by MSTest.Sdk but not used directly">
<PackageVersion Include="MSTest.Windows.AppTesting" Version="$(MSTestWindowsAppTestingVersion)" />
<PackageVersion Include="Microsoft.Playwright.MSTest.v4" Version="$(MicrosoftPlaywrightVersion)" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions TestFx.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
</Folder>
<Folder Name="/src/3 - TestFramework/">
<Project Path="src/TestFramework/TestFramework.Extensions/TestFramework.Extensions.csproj" />
<Project Path="src/TestFramework/TestFramework.Windows.AppTesting/TestFramework.Windows.AppTesting.csproj" />
<Project Path="src/TestFramework/TestFramework/TestFramework.csproj" />
</Folder>
<Folder Name="/src/4 - Analyzers/">
Expand Down
3 changes: 2 additions & 1 deletion eng/verify-nupkgs.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ function Unzip {
function Confirm-NugetPackages {
Write-Verbose "Starting Confirm-NugetPackages."
$expectedNumOfFiles = @{
"MSTest.Sdk" = 15
"MSTest.Sdk" = 16
"MSTest.TestFramework" = 105
"MSTest.TestAdapter" = 49
"MSTest" = 10
"MSTest.Analyzers" = 56
"MSTest.Windows.AppTesting" = 9
}

$packageDirectory = Resolve-Path "$PSScriptRoot/../artifacts/packages/$configuration"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Windows.Automation;

namespace ProjectUsingWindowsAppTesting;

/// <summary>
/// Sample end-to-end tests for the Windows Calculator application.
/// Demonstrates the WindowTest base class which manages app launch, main window
/// discovery, and teardown — analogous to how PageTest works for Playwright.
/// </summary>
/// <remarks>
/// <para>
/// On modern Windows 10/11, <c>calc.exe</c> launches the Store/UWP Calculator, whose
/// UI Automation tree uses different AutomationIds than the classic Win32 Calculator.
/// The <c>Calculator_NumberButtons_AreVisible</c> test below is illustrative — you will
/// need to replace the AutomationId values with the actual IDs for your target application.
/// </para>
/// </remarks>
[STATestClass]
public class CalculatorTests : WindowTest
{
/// <summary>
/// Path to the application under test.
/// Override this to point to your own application executable.
/// </summary>
public override string ApplicationPath => "calc.exe";

[TestMethod]
public void Calculator_MainWindow_HasExpectedTitle()
{
// The MainWindow property is automatically populated by WindowTest
// after launching the application specified by ApplicationPath.
Assert.AreEqual(ControlType.Window, MainWindow.Current.ControlType,
"Expected the main window element to be of control type Window.");

string title = MainWindow.Current.Name;
Assert.IsFalse(
string.IsNullOrWhiteSpace(title),
"Expected the main window to have a non-empty title.");
}

[TestMethod]
[Ignore("AutomationIds are app-version-specific. Replace 'num1Button'/'num2Button' with actual IDs for your target application.")]
public void Calculator_NumberButtons_AreVisible()
{
// NOTE: Modern Windows Calculator (UWP) uses different AutomationIds than the
// classic Win32 Calculator. Replace "num1Button" / "num2Button" with the actual
// AutomationIds for your target application version.
AutomationElement? button1 = MainWindow.FindFirst(
TreeScope.Descendants,
new PropertyCondition(AutomationElement.AutomationIdProperty, "num1Button"));

AutomationElement? button2 = MainWindow.FindFirst(
TreeScope.Descendants,
new PropertyCondition(AutomationElement.AutomationIdProperty, "num2Button"));

Assert.IsNotNull(button1, "Could not find the button with AutomationId 'num1Button'.");
Comment thread
Evangelink marked this conversation as resolved.
Assert.IsNotNull(button2, "Could not find the button with AutomationId 'num2Button'.");
}
Comment thread
Evangelink marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<Project Sdk="MSTest.Sdk">

<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Enables the MSTest Windows App Testing feature -->
<EnableWindowsAppTesting>true</EnableWindowsAppTesting>
</PropertyGroup>

Comment thread
Evangelink marked this conversation as resolved.
</Project>


<!--
Below is the equivalent project configuration when not using MSTest.Sdk

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<EnableMSTestRunner>true</EnableMSTestRunner>
<OutputType>Exe</OutputType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MSTest.Windows.AppTesting" Version="$(MSTestWindowsAppTestingVersion)" />
<PackageReference Include="MSTest.Analyzers" Version="$(MSTestVersion)" />
<PackageReference Include="MSTest.TestAdapter" Version="$(MSTestVersion)" />
<PackageReference Include="MSTest.TestFramework" Version="$(MSTestVersion)" />
</ItemGroup>

<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
<Using Include="Microsoft.MSTest.Windows.AppTesting" />
</ItemGroup>

</Project>
-->
2 changes: 1 addition & 1 deletion src/Package/MSTest.Sdk/MSTest.Sdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<_MSTestEngineVersionSuffix>$(_MSTestEnginePreReleaseVersionLabel)$(_BuildNumberLabels)</_MSTestEngineVersionSuffix>
<_MSTestEngineVersion>$(MSTestEngineVersionPrefix)</_MSTestEngineVersion>
<_MSTestEngineVersion Condition="'$(_MSTestEngineVersionSuffix)' != ''">$(_MSTestEngineVersion)-$(_MSTestEngineVersionSuffix)</_MSTestEngineVersion>
<_TemplateProperties>MSTestEngineVersion=$(_MSTestEngineVersion);MSTestVersion=$(Version);MicrosoftTestingPlatformVersion=$(Version.Replace('$(VersionPrefix)', '$(TestingPlatformVersionPrefix)'));MicrosoftNETTestSdkVersion=$(MicrosoftNETTestSdkVersion);MicrosoftTestingExtensionsCodeCoverageVersion=$(MicrosoftTestingExtensionsCodeCoverageVersion);MicrosoftPlaywrightVersion=$(MicrosoftPlaywrightVersion);AspireHostingTestingVersion=$(AspireHostingTestingVersion);MicrosoftTestingExtensionsFakesVersion=$(MicrosoftTestingExtensionsFakesVersion)</_TemplateProperties>
<_TemplateProperties>MSTestEngineVersion=$(_MSTestEngineVersion);MSTestVersion=$(Version);MicrosoftTestingPlatformVersion=$(Version.Replace('$(VersionPrefix)', '$(TestingPlatformVersionPrefix)'));MicrosoftNETTestSdkVersion=$(MicrosoftNETTestSdkVersion);MicrosoftTestingExtensionsCodeCoverageVersion=$(MicrosoftTestingExtensionsCodeCoverageVersion);MicrosoftPlaywrightVersion=$(MicrosoftPlaywrightVersion);MSTestWindowsAppTestingVersion=$(Version);AspireHostingTestingVersion=$(AspireHostingTestingVersion);MicrosoftTestingExtensionsFakesVersion=$(MicrosoftTestingExtensionsFakesVersion)</_TemplateProperties>
</PropertyGroup>

<!--
Expand Down
1 change: 1 addition & 0 deletions src/Package/MSTest.Sdk/MSTest.Sdk.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<file src="$MSBuildProjectDirectory$/Sdk/Sdk.targets" target="Sdk" />
<file src="$MSBuildProjectDirectory$/Sdk/Features/Aspire.targets" target="Sdk/Features" />
<file src="$MSBuildProjectDirectory$/Sdk/Features/Playwright.targets" target="Sdk/Features" />
<file src="$MSBuildProjectDirectory$/Sdk/Features/WindowsAppTesting.targets" target="Sdk/Features" />
<file src="$MSBuildProjectDirectory$/Sdk/Runner/Runner.targets" target="Sdk/Runner" />
<file src="$MSBuildProjectDirectory$/Sdk/Runner/Common.targets" target="Sdk/Runner" />
<file src="$MSBuildProjectDirectory$/Sdk/Runner/ClassicEngine.targets" target="Sdk/Runner" />
Expand Down
31 changes: 31 additions & 0 deletions src/Package/MSTest.Sdk/Sdk/Features/WindowsAppTesting.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<PropertyGroup>
<_IsWindowsTfm>false</_IsWindowsTfm>
<_IsWindowsTfm Condition="$(TargetFramework.Contains('-windows'))">true</_IsWindowsTfm>
</PropertyGroup>

<Target Name="_ValidateWindowsAppTestingPlatform" BeforeTargets="Restore;Build"
Condition=" '$(TargetFramework)' != '' and '$(_IsWindowsTfm)' != 'true' ">
<Error Text="MSTest.Windows.AppTesting requires a Windows target framework (e.g. net8.0-windows). Current TargetFramework: '$(TargetFramework)'." />
Comment thread
Evangelink marked this conversation as resolved.
</Target>

<ItemGroup>
<PackageReference Include="MSTest.Windows.AppTesting" Sdk="MSTest">
<Version Condition=" '$(ManagePackageVersionsCentrally)' != 'true' ">$(MSTestWindowsAppTestingVersion)</Version>
</PackageReference>
<PackageVersion Include="MSTest.Windows.AppTesting" Version="$(MSTestWindowsAppTestingVersion)"
Condition=" '$(ManagePackageVersionsCentrally)' == 'true' " />
</ItemGroup>

<!--
Implicit imports
Ensure feature is available and user hasn't opted-out from it.
See https://github.com/dotnet/sdk/blob/f9fdf2c7d94bc86dc443e5a9ffecbd1962b1d85d/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.CSharp.props#L26-L34
-->
<ItemGroup Condition=" '$(ImplicitUsings)' == 'true' OR '$(ImplicitUsings)' == 'enable' ">
<Using Include="Microsoft.MSTest.Windows.AppTesting" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions src/Package/MSTest.Sdk/Sdk/Runner/ClassicEngine.targets
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
</ItemGroup>

<Import Project="$(MSBuildThisFileDirectory)../Features/Aspire.targets" Condition=" '$(EnableAspireTesting)' == 'true' " />
<Import Project="$(MSBuildThisFileDirectory)../Features/WindowsAppTesting.targets" Condition=" '$(EnableWindowsAppTesting)' == 'true' " />
<Import Project="$(MSBuildThisFileDirectory)../Features/Playwright.targets" Condition=" '$(EnablePlaywright)' == 'true' " />

</Project>
1 change: 1 addition & 0 deletions src/Package/MSTest.Sdk/Sdk/Runner/NativeAOT.targets
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

<Target Name="_MSTestSDKValidateFeatures" BeforeTargets="Build">
<Error Condition=" '$(EnableAspireTesting)' == 'true' " Text="Aspire MSTest currently doesn't support NativeAOT mode." />
<Error Condition=" '$(EnableWindowsAppTesting)' == 'true' " Text="Windows App Testing MSTest currently doesn't support NativeAOT mode." />
<Error Condition=" '$(EnablePlaywright)' == 'true' " Text="Playwright MSTest currently doesn't support NativeAOT mode." />

<Warning Condition=" '$(EnableMicrosoftTestingExtensionsCrashDump)' == 'true' " Text="Crash dump extension might not be working well under Native AOT." />
Expand Down
2 changes: 2 additions & 0 deletions src/Package/MSTest.Sdk/Sdk/Sdk.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@

<PropertyGroup>
<EnableAspireTesting Condition=" '$(EnableAspireTesting)' == '' ">false</EnableAspireTesting>
<EnableWindowsAppTesting Condition=" '$(EnableWindowsAppTesting)' == '' ">false</EnableWindowsAppTesting>
<EnablePlaywright Condition=" '$(EnablePlaywright)' == '' ">false</EnablePlaywright>
<UseVSTest Condition=" '$(UseVSTest)' == '' " >false</UseVSTest>

<AspireHostingTestingVersion Condition=" '$(AspireHostingTestingVersion)' == '' ">${AspireHostingTestingVersion}</AspireHostingTestingVersion>
<MicrosoftNETTestSdkVersion Condition=" '$(MicrosoftNETTestSdkVersion)' == '' ">${MicrosoftNETTestSdkVersion}</MicrosoftNETTestSdkVersion>
<MSTestWindowsAppTestingVersion Condition=" '$(MSTestWindowsAppTestingVersion)' == '' ">${MSTestWindowsAppTestingVersion}</MSTestWindowsAppTestingVersion>
<MicrosoftPlaywrightVersion Condition=" '$(MicrosoftPlaywrightVersion)' == '' ">${MicrosoftPlaywrightVersion}</MicrosoftPlaywrightVersion>
<MicrosoftTestingExtensionsCodeCoverageVersion Condition=" '$(MicrosoftTestingExtensionsCodeCoverageVersion)' == '' " >${MicrosoftTestingExtensionsCodeCoverageVersion}</MicrosoftTestingExtensionsCodeCoverageVersion>
<MicrosoftTestingExtensionsFakesVersion Condition=" '$(MicrosoftTestingExtensionsFakesVersion)' == '' " >${MicrosoftTestingExtensionsFakesVersion}</MicrosoftTestingExtensionsFakesVersion>
Expand Down
1 change: 1 addition & 0 deletions src/Package/MSTest.Sdk/Sdk/VSTest/VSTest.targets
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
</ItemGroup>

<Import Project="$(MSBuildThisFileDirectory)../Features/Aspire.targets" Condition=" '$(EnableAspireTesting)' == 'true' " />
<Import Project="$(MSBuildThisFileDirectory)../Features/WindowsAppTesting.targets" Condition=" '$(EnableWindowsAppTesting)' == 'true' " />
<Import Project="$(MSBuildThisFileDirectory)../Features/Playwright.targets" Condition=" '$(EnablePlaywright)' == 'true' " />

</Project>
134 changes: 134 additions & 0 deletions src/TestFramework/TestFramework.Windows.AppTesting/ApplicationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.ComponentModel;

namespace Microsoft.MSTest.Windows.AppTesting;

/// <summary>
/// Base test class that manages the lifecycle of a desktop application process.
/// Analogous to <c>BrowserTest</c> in the Playwright MSTest integration.
/// </summary>
/// <remarks>
/// Override <see cref="ApplicationPath"/> and optionally <see cref="ApplicationArguments"/>
/// to configure which application to launch.
/// If <see cref="ApplicationPath"/> is not overridden, the test will attempt to read
/// the path from the <c>DESKTOP_TEST_APP_PATH</c> environment variable.
/// </remarks>
[STATestClass]
public class ApplicationTest : AutomationTest
{
private const string AppPathEnvVar = "DESKTOP_TEST_APP_PATH";

/// <summary>
/// Gets the process of the application under test.
/// Available after <see cref="ApplicationSetup"/> has run.
/// </summary>
public Process AppProcess { get; private set; } = null!;

/// <summary>
/// Gets the path to the application executable to launch.
/// Override this property to specify the application under test.
/// Defaults to the <c>DESKTOP_TEST_APP_PATH</c> environment variable.
/// </summary>
public virtual string ApplicationPath
{
get
{
string? envPath = Environment.GetEnvironmentVariable(AppPathEnvVar);
return string.IsNullOrWhiteSpace(envPath)
? throw new InvalidOperationException(
$"Override {nameof(ApplicationPath)} or set the '{AppPathEnvVar}' environment variable to the path of the application under test.")
: envPath;
}
}

/// <summary>
/// Gets the command-line arguments to pass when launching the application.
/// Override to customize. Defaults to <see langword="null"/> (no arguments).
/// </summary>
public virtual string? ApplicationArguments => null;

/// <summary>
/// Gets the timeout to wait for the application's main window to become available.
/// Override to customize. Defaults to 10 seconds.
/// </summary>
public virtual TimeSpan ApplicationStartTimeout => TimeSpan.FromSeconds(10);

/// <summary>
/// Launches the application under test before each test method.
/// </summary>
[TestInitialize]
public void ApplicationSetup()
{
ProcessStartInfo startInfo = new(ApplicationPath);
if (ApplicationArguments is not null)
{
startInfo.Arguments = ApplicationArguments;
}

AppProcess = Process.Start(startInfo)
?? throw new InvalidOperationException($"Failed to start process: {ApplicationPath}");

// Wait for the main window to be created
var sw = Stopwatch.StartNew();
while (AppProcess.MainWindowHandle == IntPtr.Zero && sw.Elapsed < ApplicationStartTimeout)
{
Comment thread
Evangelink marked this conversation as resolved.
AppProcess.Refresh();
if (AppProcess.HasExited)
{
throw new InvalidOperationException(
$"Application '{ApplicationPath}' exited with code {AppProcess.ExitCode} before a main window was created.");
}

TimeSpan remainingTime = ApplicationStartTimeout - sw.Elapsed;
if (remainingTime > TimeSpan.Zero)
{
Thread.Sleep(remainingTime < TimeSpan.FromMilliseconds(50)
? remainingTime
: TimeSpan.FromMilliseconds(50));
}
}

AppProcess.Refresh();
if (AppProcess.MainWindowHandle == IntPtr.Zero)
Comment thread
Evangelink marked this conversation as resolved.
{
throw new TimeoutException(
$"Application '{ApplicationPath}' did not create a main window within {ApplicationStartTimeout}.");
}
}

/// <summary>
/// Closes and disposes the application after each test method.
/// </summary>
[TestCleanup]
public void ApplicationTearDown()
{
Process? appProcess = AppProcess;

try
{
if (appProcess is not null && !appProcess.HasExited)
{
try
{
_ = appProcess.CloseMainWindow();
if (!appProcess.WaitForExit(5000))
{
appProcess.Kill(entireProcessTree: true);
_ = appProcess.WaitForExit(5000);
}
Comment thread
Evangelink marked this conversation as resolved.
}
catch (Exception ex) when (ex is InvalidOperationException or Win32Exception)
{
// The process exited or became inaccessible between state checks and shutdown operations.
}
}
}
finally
{
appProcess?.Dispose();
AppProcess = null!;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.MSTest.Windows.AppTesting;

/// <summary>
/// Base test class for desktop UI automation tests.
/// This is the root of the desktop testing class hierarchy, analogous to
/// <c>PlaywrightTest</c> in the Playwright MSTest integration.
/// </summary>
/// <remarks>
/// This layer exists to match the Playwright base class hierarchy pattern
/// and provides the extension point for future automation-level configuration.
/// </remarks>
[TestClass]
public class AutomationTest
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

global using Microsoft.VisualStudio.TestTools.UnitTesting;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#nullable enable

Loading
Loading