Skip to content

Commit 70a249b

Browse files
authored
Merge pull request #118 from FlaUI/clipboard-and-element-property
Support clipboard extensions and element get property
2 parents 6d7245a + 7026514 commit 70a249b

File tree

9 files changed

+182
-2
lines changed

9 files changed

+182
-2
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ const result = driver.executeScript("powerShell", [{ command: `1+1` }]);
181181

182182
## Windows extensions
183183

184-
To enable easy switching from appium-windows-driver, there is a rudimentary implementation of `windows: click`, `windows: hover`, `windows: scroll` and `windows: keys`.
184+
To enable easy switching from appium-windows-driver, there is a rudimentary implementation of `windows: click`, `windows: hover`, `windows: scroll`, `windows: keys`, `windows: getClipboard` and `windows: setClipboard`.
185185

186186
## Supported WebDriver Commands
187187

@@ -221,7 +221,7 @@ To enable easy switching from appium-windows-driver, there is a rudimentary impl
221221
| GET | /session/{session id}/element/{element id}/selected | Is Element Selected | :white_check_mark: |
222222
| GET | /session/{session id}/element/{element id}/displayed | Is Element Displayed | :white_check_mark: [^isdisplayed] |
223223
| GET | /session/{session id}/element/{element id}/attribute/{name} | Get Element Attribute | :white_check_mark: [^getattribute] |
224-
| GET | /session/{session id}/element/{element id}/property/{name} | Get Element Property | |
224+
| GET | /session/{session id}/element/{element id}/property/{name} | Get Element Property | :white_check_mark: |
225225
| GET | /session/{session id}/element/{element id}/css/{property name} | Get Element CSS Value | N/A |
226226
| GET | /session/{session id}/element/{element id}/text | Get Element Text | :white_check_mark: |
227227
| GET | /session/{session id}/element/{element id}/name | Get Element Tag Name | :white_check_mark: |

src/FlaUI.WebDriver.UITests/ElementTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,20 @@ public void GetAttribute_TextBox_ReturnsValue(string attributeName, string expec
366366
Assert.That(value, Is.EqualTo(expectedValue));
367367
}
368368

369+
[TestCase(["ClassName", "TextBox"])]
370+
[TestCase(["FrameworkId", "WPF"])]
371+
[TestCase(["NonExistent", null])]
372+
public void GetProperty_TextBox_ReturnsValue(string attributeName, string expectedValue)
373+
{
374+
var driverOptions = FlaUIDriverOptions.TestApp();
375+
using var driver = new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions);
376+
var element = driver.FindElement(ExtendedBy.AccessibilityId("TextBox"));
377+
378+
var value = element.GetDomProperty(attributeName);
379+
380+
Assert.That(value, Is.EqualTo(expectedValue));
381+
}
382+
369383
[Test]
370384
public void GetAttribute_DesktopElement_ReturnsAttribute()
371385
{
@@ -395,5 +409,23 @@ public void GetAttribute_PatternProperty_ReturnsValue()
395409

396410
Assert.That(value, Is.EqualTo("On"));
397411
}
412+
413+
[Test]
414+
public void GetProperty_PatternProperty_ReturnsValue()
415+
{
416+
var driverOptions = FlaUIDriverOptions.TestApp();
417+
using var driver = new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions);
418+
var element = driver.FindElement(ExtendedBy.AccessibilityId("SimpleCheckBox"));
419+
420+
var value = element.GetDomProperty("Toggle.ToggleState");
421+
422+
Assert.That(value, Is.EqualTo("Off"));
423+
424+
element.Click();
425+
426+
value = element.GetDomProperty("Toggle.ToggleState");
427+
428+
Assert.That(value, Is.EqualTo("On"));
429+
}
398430
}
399431
}

src/FlaUI.WebDriver.UITests/ExecuteTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using FlaUI.WebDriver.UITests.TestUtil;
22
using NUnit.Framework;
3+
using OpenQA.Selenium;
4+
using OpenQA.Selenium.Interactions;
35
using OpenQA.Selenium.Remote;
46
using System.Collections.Generic;
57

@@ -32,6 +34,36 @@ public void ExecuteScript_WindowsClickXY_IsSupported()
3234
Assert.That(activeElementText, Is.EqualTo("Test TextBox"));
3335
}
3436

37+
[Test]
38+
public void ExecuteScript_WindowsGetClipboard_IsSupported()
39+
{
40+
var driverOptions = FlaUIDriverOptions.TestApp();
41+
using var driver = new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions);
42+
var element = driver.FindElement(ExtendedBy.AccessibilityId("TextBox"));
43+
element.Click();
44+
new Actions(driver).KeyDown(Keys.Control).SendKeys("a").KeyUp(Keys.Control).Perform();
45+
new Actions(driver).KeyDown(Keys.Control).SendKeys("c").KeyUp(Keys.Control).Perform();
46+
47+
var result = driver.ExecuteScript("windows: getClipboard", new Dictionary<string, object> {});
48+
49+
Assert.That(result, Is.EqualTo("Test TextBox"));
50+
}
51+
52+
[Test]
53+
public void ExecuteScript_WindowsSetClipboard_IsSupported()
54+
{
55+
var driverOptions = FlaUIDriverOptions.TestApp();
56+
using var driver = new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions);
57+
58+
var result = driver.ExecuteScript("windows: setClipboard", new Dictionary<string, object> {
59+
["b64Content"] = "Pasted!"});
60+
61+
var element = driver.FindElement(ExtendedBy.AccessibilityId("TextBox"));
62+
element.Click();
63+
new Actions(driver).KeyDown(Keys.Control).SendKeys("v").KeyUp(Keys.Control).Perform();
64+
Assert.That(element.Text, Is.EqualTo("Test TextBoxPasted!"));
65+
}
66+
3567
[Test]
3668
public void ExecuteScript_WindowsHoverXY_IsSupported()
3769
{

src/FlaUI.WebDriver/Controllers/ElementController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ public async Task<ActionResult> ElementSendKeys([FromRoute] string sessionId, [F
229229
}
230230

231231
[HttpGet("{elementId}/attribute/{attributeId}")]
232+
[HttpGet("{elementId}/property/{attributeId}")]
232233
public async Task<ActionResult> GetAttribute([FromRoute] string sessionId, [FromRoute] string elementId, [FromRoute] string attributeId)
233234
{
234235
var session = GetSession(sessionId);

src/FlaUI.WebDriver/Controllers/ExecuteController.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ public async Task<ActionResult> ExecuteScript([FromRoute] string sessionId, [Fro
3737
return await ExecuteWindowsHoverScript(session, executeScriptRequest);
3838
case "windows: scroll":
3939
return await ExecuteWindowsScrollScript(session, executeScriptRequest);
40+
case "windows: setClipboard":
41+
return await ExecuteWindowsSetClipboardScript(session, executeScriptRequest);
42+
case "windows: getClipboard":
43+
return await ExecuteWindowsGetClipboardScript(session, executeScriptRequest);
4044
default:
4145
throw WebDriverResponseException.UnsupportedOperation("Only 'powerShell', 'windows: keys', 'windows: click', 'windows: hover' scripts are supported");
4246
}
@@ -90,6 +94,36 @@ private async Task<ActionResult> ExecutePowerShellScript(Session session, Execut
9094
return WebDriverResult.Success(result);
9195
}
9296

97+
private async Task<ActionResult> ExecuteWindowsSetClipboardScript(Session session, ExecuteScriptRequest executeScriptRequest)
98+
{
99+
if (executeScriptRequest.Args.Count != 1)
100+
{
101+
throw WebDriverResponseException.InvalidArgument($"Expected an array of exactly 1 arguments for the windows: setClipboard script, but got {executeScriptRequest.Args.Count} arguments");
102+
}
103+
var action = JsonSerializer.Deserialize<WindowsSetClipboardScript>(executeScriptRequest.Args[0], new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
104+
if (action == null)
105+
{
106+
throw WebDriverResponseException.InvalidArgument("Action cannot be null");
107+
}
108+
await _windowsExtensionService.ExecuteSetClipboardScript(session, action);
109+
return WebDriverResult.Success();
110+
}
111+
112+
private async Task<ActionResult> ExecuteWindowsGetClipboardScript(Session session, ExecuteScriptRequest executeScriptRequest)
113+
{
114+
if (executeScriptRequest.Args.Count != 1)
115+
{
116+
throw WebDriverResponseException.InvalidArgument($"Expected an array of exactly 1 arguments for the windows: getClipboard script, but got {executeScriptRequest.Args.Count} arguments");
117+
}
118+
var action = JsonSerializer.Deserialize<WindowsGetClipboardScript>(executeScriptRequest.Args[0], new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
119+
if (action == null)
120+
{
121+
throw WebDriverResponseException.InvalidArgument("Action cannot be null");
122+
}
123+
var result = await _windowsExtensionService.ExecuteGetClipboardScript(session, action);
124+
return WebDriverResult.Success(result);
125+
}
126+
93127
private async Task<ActionResult> ExecuteWindowsClickScript(Session session, ExecuteScriptRequest executeScriptRequest)
94128
{
95129
if (executeScriptRequest.Args.Count != 1)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace FlaUI.WebDriver.Models
2+
{
3+
public class WindowsGetClipboardScript
4+
{
5+
public string? ContentType { get; set; }
6+
}
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace FlaUI.WebDriver.Models
2+
{
3+
public class WindowsSetClipboardScript
4+
{
5+
public string B64Content { get; set; } = "";
6+
public string? ContentType { get; set; }
7+
}
8+
}

src/FlaUI.WebDriver/Services/IWindowsExtensionService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ public interface IWindowsExtensionService
88
Task ExecuteScrollScript(Session session, WindowsScrollScript action);
99
Task ExecuteHoverScript(Session session, WindowsHoverScript action);
1010
Task ExecuteKeyScript(Session session, WindowsKeyScript action);
11+
Task<string> ExecuteGetClipboardScript(Session session, WindowsGetClipboardScript action);
12+
Task ExecuteSetClipboardScript(Session session, WindowsSetClipboardScript action);
1113
}
1214
}

src/FlaUI.WebDriver/Services/WindowsExtensionService.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,70 @@ public WindowsExtensionService(ILogger<WindowsExtensionService> logger)
1515
_logger = logger;
1616
}
1717

18+
public Task<string> ExecuteGetClipboardScript(Session session, WindowsGetClipboardScript action)
19+
{
20+
switch(action.ContentType)
21+
{
22+
default:
23+
case "plaintext":
24+
return Task.FromResult(ExecuteOnClipboardThread(
25+
() => System.Windows.Forms.Clipboard.GetText(System.Windows.Forms.TextDataFormat.UnicodeText)
26+
));
27+
case "image":
28+
return Task.FromResult(ExecuteOnClipboardThread(() =>
29+
{
30+
using var image = System.Windows.Forms.Clipboard.GetImage();
31+
if (image == null)
32+
{
33+
return "";
34+
}
35+
using var stream = new MemoryStream();
36+
image.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
37+
return Convert.ToBase64String(stream.ToArray());
38+
}));
39+
}
40+
}
41+
42+
public Task ExecuteSetClipboardScript(Session session, WindowsSetClipboardScript action)
43+
{
44+
switch (action.ContentType)
45+
{
46+
default:
47+
case "plaintext":
48+
ExecuteOnClipboardThread(() => System.Windows.Forms.Clipboard.SetText(action.B64Content));
49+
break;
50+
case "image":
51+
ExecuteOnClipboardThread(() =>
52+
{
53+
using var stream = new MemoryStream(Convert.FromBase64String(action.B64Content));
54+
using var image = Image.FromStream(stream);
55+
System.Windows.Forms.Clipboard.SetImage(image);
56+
});
57+
break;
58+
}
59+
return Task.CompletedTask;
60+
}
61+
62+
private void ExecuteOnClipboardThread(System.Action action)
63+
{
64+
// See https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.clipboard?view=windowsdesktop-8.0#remarks
65+
var thread = new Thread(() => action());
66+
thread.SetApartmentState(ApartmentState.STA);
67+
thread.Start();
68+
thread.Join();
69+
}
70+
71+
private string ExecuteOnClipboardThread(Func<string> method)
72+
{
73+
// See https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.clipboard?view=windowsdesktop-8.0#remarks
74+
string result = "";
75+
var thread = new Thread(() => { result = method(); });
76+
thread.SetApartmentState(ApartmentState.STA);
77+
thread.Start();
78+
thread.Join();
79+
return result;
80+
}
81+
1882
public async Task ExecuteClickScript(Session session, WindowsClickScript action)
1983
{
2084
if (action.DurationMs.HasValue)

0 commit comments

Comments
 (0)