-
Notifications
You must be signed in to change notification settings - Fork 301
Expand file tree
/
Copy pathApplicationTest.cs
More file actions
134 lines (120 loc) · 4.8 KB
/
Copy pathApplicationTest.cs
File metadata and controls
134 lines (120 loc) · 4.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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)
{
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)
{
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);
}
}
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!;
}
}
}