Skip to content

Commit fef5489

Browse files
authored
Merge pull request #16 from FlaUI/appium-new-command-timeout
Add new session timeout feature
2 parents cee32df + 4a727d0 commit fef5489

21 files changed

+302
-32
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The following capabilities are supported:
2828
| appium:appArguments | Application arguments string, for example `/?`. | |
2929
| appium:appTopLevelWindow | The hexadecimal handle of an existing application top level window to attach to, for example `0x12345` (should be of string type). Either this capability, `appTopLevelWindowTitleMatch` or `app` must be provided on session startup. | `0xC0B46` |
3030
| appium:appTopLevelWindowTitleMatch | The title of an existing application top level window to attach to, for example `My App Window Title` (should be of string type). Either this capability, `appTopLevelWindow` or `app` must be provided on session startup. | `My App Window Title` or `My App Window Title - .*` |
31+
| appium:newCommandTimeout | The number of seconds the to wait for clients to send commands before deciding that the client has gone away and the session should shut down. Default one minute (60). | `120` |
3132

3233
## Getting Started
3334

src/FlaUI.WebDriver.UITests/SessionTests.cs

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ public void NewSession_AppNotExists_ReturnsError()
4040
Assert.That(newSession, Throws.TypeOf<WebDriverArgumentException>().With.Message.EqualTo("Starting app 'C:\\NotExisting.exe' with arguments '' threw an exception: An error occurred trying to start process 'C:\\NotExisting.exe' with working directory '.'. The system cannot find the file specified."));
4141
}
4242

43+
[TestCase(123)]
44+
[TestCase(false)]
45+
public void NewSession_AppNotAString_Throws(object value)
46+
{
47+
var driverOptions = new FlaUIDriverOptions()
48+
{
49+
PlatformName = "Windows"
50+
};
51+
driverOptions.AddAdditionalOption("appium:app", value);
52+
53+
Assert.That(() => new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions),
54+
Throws.TypeOf<WebDriverArgumentException>().With.Message.EqualTo("Capability appium:app must be a string"));
55+
}
56+
4357
[Test]
4458
public void NewSession_AppTopLevelWindow_IsSupported()
4559
{
@@ -91,7 +105,7 @@ public void NewSession_AppTopLevelWindowTitleMatch_IsSupported(string match)
91105
}
92106

93107
[Test, Ignore("Sometimes multiple processes are left open")]
94-
public void NewSession_MultipleMatchingAppTopLevelWindowTitleMatch_ReturnsError()
108+
public void NewSession_AppTopLevelWindowTitleMatchMultipleMatching_ReturnsError()
95109
{
96110
using var testAppProcess = new TestAppProcess();
97111
using var testAppProcess1 = new TestAppProcess();
@@ -125,6 +139,33 @@ public void NewSession_AppTopLevelWindowTitleMatchNotFound_ReturnsError()
125139
Assert.That(newSession, Throws.TypeOf<WebDriverArgumentException>().With.Message.EqualTo("Process with main window title matching 'FlaUI Not Existing' could not be found"));
126140
}
127141

142+
[TestCase(123)]
143+
[TestCase(false)]
144+
public void NewSession_AppTopLevelWindowTitleMatchNotAString_Throws(object value)
145+
{
146+
var driverOptions = new FlaUIDriverOptions()
147+
{
148+
PlatformName = "Windows"
149+
};
150+
driverOptions.AddAdditionalOption("appium:appTopLevelWindowTitleMatch", value);
151+
152+
Assert.That(() => new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions),
153+
Throws.TypeOf<WebDriverArgumentException>().With.Message.EqualTo("Capability appium:appTopLevelWindowTitleMatch must be a string"));
154+
}
155+
156+
[TestCase("(invalid")]
157+
public void NewSession_AppTopLevelWindowTitleMatchInvalidRegex_Throws(string value)
158+
{
159+
var driverOptions = new FlaUIDriverOptions()
160+
{
161+
PlatformName = "Windows"
162+
};
163+
driverOptions.AddAdditionalOption("appium:appTopLevelWindowTitleMatch", value);
164+
165+
Assert.That(() => new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions),
166+
Throws.TypeOf<WebDriverArgumentException>().With.Message.EqualTo("Capability appium:appTopLevelWindowTitleMatch '(invalid' is not a valid regular expression: Invalid pattern '(invalid' at offset 8. Not enough )'s."));
167+
}
168+
128169
[TestCase("")]
129170
[TestCase("FlaUI")]
130171
public void NewSession_AppTopLevelWindowInvalidFormat_ReturnsError(string appTopLevelWindowString)
@@ -136,6 +177,20 @@ public void NewSession_AppTopLevelWindowInvalidFormat_ReturnsError(string appTop
136177
Assert.That(newSession, Throws.TypeOf<WebDriverArgumentException>().With.Message.EqualTo($"Capability appium:appTopLevelWindow '{appTopLevelWindowString}' is not a valid hexadecimal string"));
137178
}
138179

180+
[TestCase(123)]
181+
[TestCase(false)]
182+
public void NewSession_AppTopLevelWindowNotAString_ReturnsError(object value)
183+
{
184+
var driverOptions = new FlaUIDriverOptions()
185+
{
186+
PlatformName = "Windows"
187+
};
188+
driverOptions.AddAdditionalOption("appium:appTopLevelWindow", value);
189+
190+
Assert.That(() => new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions),
191+
Throws.TypeOf<WebDriverArgumentException>().With.Message.EqualTo("Capability appium:appTopLevelWindow must be a string"));
192+
}
193+
139194
[Test]
140195
public void GetTitle_Default_IsSupported()
141196
{
@@ -146,5 +201,68 @@ public void GetTitle_Default_IsSupported()
146201

147202
Assert.That(title, Is.EqualTo("FlaUI WPF Test App"));
148203
}
204+
205+
[Test, Explicit("Takes too long (one minute)")]
206+
public void NewCommandTimeout_DefaultValue_OneMinute()
207+
{
208+
var driverOptions = FlaUIDriverOptions.TestApp();
209+
using var driver = new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions);
210+
211+
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(60) + WebDriverFixture.SessionCleanupInterval*2);
212+
213+
Assert.That(() => driver.Title, Throws.TypeOf<WebDriverException>().With.Message.Matches("No active session with ID '.*'"));
214+
}
215+
216+
[Test]
217+
public void NewCommandTimeout_Expired_EndsSession()
218+
{
219+
var driverOptions = FlaUIDriverOptions.TestApp();
220+
driverOptions.AddAdditionalOption("appium:newCommandTimeout", 1);
221+
using var driver = new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions);
222+
223+
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(1) + WebDriverFixture.SessionCleanupInterval * 2);
224+
225+
Assert.That(() => driver.Title, Throws.TypeOf<WebDriverException>().With.Message.Matches("No active session with ID '.*'"));
226+
}
227+
228+
[Test]
229+
public void NewCommandTimeout_ReceivedCommandsBeforeExpiry_DoesNotEndSession()
230+
{
231+
var driverOptions = FlaUIDriverOptions.TestApp();
232+
driverOptions.AddAdditionalOption("appium:newCommandTimeout", WebDriverFixture.SessionCleanupInterval.TotalSeconds * 4);
233+
using var driver = new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions);
234+
235+
System.Threading.Thread.Sleep(WebDriverFixture.SessionCleanupInterval * 2);
236+
_ = driver.Title;
237+
System.Threading.Thread.Sleep(WebDriverFixture.SessionCleanupInterval * 2);
238+
_ = driver.Title;
239+
System.Threading.Thread.Sleep(WebDriverFixture.SessionCleanupInterval * 2);
240+
241+
Assert.That(() => driver.Title, Throws.Nothing);
242+
}
243+
244+
[Test]
245+
public void NewCommandTimeout_NotExpired_DoesNotEndSession()
246+
{
247+
var driverOptions = FlaUIDriverOptions.TestApp();
248+
driverOptions.AddAdditionalOption("appium:newCommandTimeout", 240);
249+
using var driver = new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions);
250+
251+
System.Threading.Thread.Sleep(WebDriverFixture.SessionCleanupInterval * 2);
252+
253+
Assert.That(() => driver.Title, Throws.Nothing);
254+
}
255+
256+
[TestCase("123")]
257+
[TestCase(false)]
258+
[TestCase("not a number")]
259+
public void NewCommandTimeout_InvalidValue_Throws(object value)
260+
{
261+
var driverOptions = FlaUIDriverOptions.TestApp();
262+
driverOptions.AddAdditionalOption("appium:newCommandTimeout", value);
263+
264+
Assert.That(() => new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions),
265+
Throws.TypeOf<WebDriverArgumentException>().With.Message.EqualTo("Capability appium:newCommandTimeout must be a number"));
266+
}
149267
}
150268
}

src/FlaUI.WebDriver.UITests/WebDriverFixture.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ namespace FlaUI.WebDriver.UITests
99
[SetUpFixture]
1010
public class WebDriverFixture
1111
{
12-
public static readonly Uri WebDriverUrl = new Uri("http://localhost:9723/");
12+
public static readonly Uri WebDriverUrl = new Uri("http://localhost:4723/");
13+
public static readonly TimeSpan SessionCleanupInterval = TimeSpan.FromSeconds(1);
1314

1415
private Process _webDriverProcess;
1516

@@ -20,7 +21,7 @@ public void Setup()
2021
Directory.SetCurrentDirectory(assemblyDir);
2122

2223
string webDriverPath = Path.Combine(Directory.GetCurrentDirectory(), "FlaUI.WebDriver.exe");
23-
var webDriverArguments = $"--urls={WebDriverUrl}";
24+
var webDriverArguments = $"--urls={WebDriverUrl} --SessionCleanup:SchedulingIntervalSeconds={SessionCleanupInterval.TotalSeconds}";
2425
var webDriverProcessStartInfo = new ProcessStartInfo(webDriverPath, webDriverArguments)
2526
{
2627
RedirectStandardError = true,

src/FlaUI.WebDriver/Controllers/ActionsController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ private Session GetSession(string sessionId)
333333
{
334334
throw WebDriverResponseException.SessionNotFound(sessionId);
335335
}
336+
session.SetLastCommandTimeToNow();
336337
return session;
337338
}
338339
}

src/FlaUI.WebDriver/Controllers/ElementController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ private Session GetSession(string sessionId)
219219
{
220220
throw WebDriverResponseException.SessionNotFound(sessionId);
221221
}
222+
session.SetLastCommandTimeToNow();
222223
return session;
223224
}
224225
}

src/FlaUI.WebDriver/Controllers/ExecuteController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ private Session GetSession(string sessionId)
7878
{
7979
throw WebDriverResponseException.SessionNotFound(sessionId);
8080
}
81+
session.SetLastCommandTimeToNow();
8182
return session;
8283
}
8384
}

src/FlaUI.WebDriver/Controllers/FindElementsController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ private Session GetSession(string sessionId)
210210
{
211211
throw WebDriverResponseException.SessionNotFound(sessionId);
212212
}
213+
session.SetLastCommandTimeToNow();
213214
return session;
214215
}
215216
}

src/FlaUI.WebDriver/Controllers/ScreenshotController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ private Session GetSession(string sessionId)
7575
{
7676
throw WebDriverResponseException.SessionNotFound(sessionId);
7777
}
78+
session.SetLastCommandTimeToNow();
7879
return session;
7980
}
8081
}

src/FlaUI.WebDriver/Controllers/SessionController.cs

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
78
using System.Linq;
89
using System.Text.Json;
910
using System.Text.RegularExpressions;
@@ -29,7 +30,7 @@ public async Task<ActionResult> CreateNewSession([FromBody] CreateSessionRequest
2930
{
3031
var possibleCapabilities = GetPossibleCapabilities(request);
3132
var matchingCapabilities = possibleCapabilities.Where(
32-
capabilities => capabilities.TryGetValue("platformName", out var platformName) && platformName.ToLowerInvariant() == "windows"
33+
capabilities => capabilities.TryGetValue("platformName", out var platformName) && platformName.GetString()?.ToLowerInvariant() == "windows"
3334
);
3435

3536
Core.Application? app;
@@ -42,15 +43,15 @@ public async Task<ActionResult> CreateNewSession([FromBody] CreateSessionRequest
4243
Message = "Required capabilities did not match. Capability `platformName` with value `windows` is required"
4344
});
4445
}
45-
if (capabilities.TryGetValue("appium:app", out var appPath))
46+
if (TryGetStringCapability(capabilities, "appium:app", out var appPath))
4647
{
4748
if (appPath == "Root")
4849
{
4950
app = null;
5051
}
5152
else
52-
{
53-
capabilities.TryGetValue("appium:appArguments", out var appArguments);
53+
{
54+
TryGetStringCapability(capabilities, "appium:appArguments", out var appArguments);
5455
try
5556
{
5657
if (appPath.EndsWith("!App"))
@@ -69,12 +70,12 @@ public async Task<ActionResult> CreateNewSession([FromBody] CreateSessionRequest
6970
}
7071
}
7172
}
72-
else if(capabilities.TryGetValue("appium:appTopLevelWindow", out var appTopLevelWindowString))
73+
else if (TryGetStringCapability(capabilities, "appium:appTopLevelWindow", out var appTopLevelWindowString))
7374
{
7475
Process process = GetProcessByMainWindowHandle(appTopLevelWindowString);
7576
app = Core.Application.Attach(process);
7677
}
77-
else if (capabilities.TryGetValue("appium:appTopLevelWindowTitleMatch", out var appTopLevelWindowTitleMatch))
78+
else if (TryGetStringCapability(capabilities, "appium:appTopLevelWindowTitleMatch", out var appTopLevelWindowTitleMatch))
7879
{
7980
Process? process = GetProcessByMainWindowTitle(appTopLevelWindowTitleMatch);
8081
app = Core.Application.Attach(process);
@@ -84,6 +85,10 @@ public async Task<ActionResult> CreateNewSession([FromBody] CreateSessionRequest
8485
throw WebDriverResponseException.InvalidArgument("One of appium:app, appium:appTopLevelWindow or appium:appTopLevelWindowTitleMatch must be passed as a capability");
8586
}
8687
var session = new Session(app);
88+
if(TryGetNumberCapability(capabilities, "appium:newCommandTimeout", out var newCommandTimeout))
89+
{
90+
session.NewCommandTimeout = TimeSpan.FromSeconds(newCommandTimeout);
91+
}
8792
_sessionRepository.Add(session);
8893
_logger.LogInformation("Created session with ID {SessionId} and capabilities {Capabilities}", session.SessionId, capabilities);
8994
return await Task.FromResult(WebDriverResult.Success(new CreateSessionResponse()
@@ -93,6 +98,40 @@ public async Task<ActionResult> CreateNewSession([FromBody] CreateSessionRequest
9398
}));
9499
}
95100

101+
private static bool TryGetStringCapability(Dictionary<string, JsonElement> capabilities, string key, [MaybeNullWhen(false)] out string value)
102+
{
103+
if(capabilities.TryGetValue(key, out var valueJson))
104+
{
105+
if(valueJson.ValueKind != JsonValueKind.String)
106+
{
107+
throw WebDriverResponseException.InvalidArgument($"Capability {key} must be a string");
108+
}
109+
110+
value = valueJson.GetString();
111+
return value != null;
112+
}
113+
114+
value = null;
115+
return false;
116+
}
117+
118+
private static bool TryGetNumberCapability(Dictionary<string, JsonElement> capabilities, string key, out double value)
119+
{
120+
if (capabilities.TryGetValue(key, out var valueJson))
121+
{
122+
if (valueJson.ValueKind != JsonValueKind.Number)
123+
{
124+
throw WebDriverResponseException.InvalidArgument($"Capability {key} must be a number");
125+
}
126+
127+
value = valueJson.GetDouble();
128+
return true;
129+
}
130+
131+
value = default;
132+
return false;
133+
}
134+
96135
private static Process GetProcessByMainWindowTitle(string appTopLevelWindowTitleMatch)
97136
{
98137
Regex appMainWindowTitleRegex;
@@ -139,14 +178,14 @@ private static Process GetProcessByMainWindowHandle(string appTopLevelWindowStri
139178
return process;
140179
}
141180

142-
private static IEnumerable<Dictionary<string, string>> GetPossibleCapabilities(CreateSessionRequest request)
181+
private static IEnumerable<Dictionary<string, JsonElement>> GetPossibleCapabilities(CreateSessionRequest request)
143182
{
144-
var requiredCapabilities = request.Capabilities.AlwaysMatch ?? new Dictionary<string, string>();
145-
var allFirstMatchCapabilities = request.Capabilities.FirstMatch ?? new List<Dictionary<string, string>>(new[] { new Dictionary<string, string>() });
183+
var requiredCapabilities = request.Capabilities.AlwaysMatch ?? new Dictionary<string, JsonElement>();
184+
var allFirstMatchCapabilities = request.Capabilities.FirstMatch ?? new List<Dictionary<string, JsonElement>>(new[] { new Dictionary<string, JsonElement>() });
146185
return allFirstMatchCapabilities.Select(firstMatchCapabilities => MergeCapabilities(firstMatchCapabilities, requiredCapabilities));
147186
}
148187

149-
private static Dictionary<string, string> MergeCapabilities(Dictionary<string, string> firstMatchCapabilities, Dictionary<string, string> requiredCapabilities)
188+
private static Dictionary<string, JsonElement> MergeCapabilities(Dictionary<string, JsonElement> firstMatchCapabilities, Dictionary<string, JsonElement> requiredCapabilities)
150189
{
151190
var duplicateKeys = firstMatchCapabilities.Keys.Intersect(requiredCapabilities.Keys);
152191
if (duplicateKeys.Any())
@@ -183,6 +222,7 @@ private Session GetSession(string sessionId)
183222
{
184223
throw WebDriverResponseException.SessionNotFound(sessionId);
185224
}
225+
session.SetLastCommandTimeToNow();
186226
return session;
187227
}
188228
}

src/FlaUI.WebDriver/Controllers/TimeoutsController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ private Session GetSession(string sessionId)
4242
{
4343
throw WebDriverResponseException.SessionNotFound(sessionId);
4444
}
45+
session.SetLastCommandTimeToNow();
4546
return session;
4647
}
4748
}

0 commit comments

Comments
 (0)