Skip to content

Commit

Permalink
Merge pull request #118 from FlaUI/clipboard-and-element-property
Browse files Browse the repository at this point in the history
Support clipboard extensions and element get property
  • Loading branch information
aristotelos authored Dec 13, 2024
2 parents 6d7245a + 7026514 commit 70a249b
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 2 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ const result = driver.executeScript("powerShell", [{ command: `1+1` }]);

## Windows extensions

To enable easy switching from appium-windows-driver, there is a rudimentary implementation of `windows: click`, `windows: hover`, `windows: scroll` and `windows: keys`.
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`.

## Supported WebDriver Commands

Expand Down Expand Up @@ -221,7 +221,7 @@ To enable easy switching from appium-windows-driver, there is a rudimentary impl
| GET | /session/{session id}/element/{element id}/selected | Is Element Selected | :white_check_mark: |
| GET | /session/{session id}/element/{element id}/displayed | Is Element Displayed | :white_check_mark: [^isdisplayed] |
| GET | /session/{session id}/element/{element id}/attribute/{name} | Get Element Attribute | :white_check_mark: [^getattribute] |
| GET | /session/{session id}/element/{element id}/property/{name} | Get Element Property | |
| GET | /session/{session id}/element/{element id}/property/{name} | Get Element Property | :white_check_mark: |
| GET | /session/{session id}/element/{element id}/css/{property name} | Get Element CSS Value | N/A |
| GET | /session/{session id}/element/{element id}/text | Get Element Text | :white_check_mark: |
| GET | /session/{session id}/element/{element id}/name | Get Element Tag Name | :white_check_mark: |
Expand Down
32 changes: 32 additions & 0 deletions src/FlaUI.WebDriver.UITests/ElementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,20 @@ public void GetAttribute_TextBox_ReturnsValue(string attributeName, string expec
Assert.That(value, Is.EqualTo(expectedValue));
}

[TestCase(["ClassName", "TextBox"])]
[TestCase(["FrameworkId", "WPF"])]
[TestCase(["NonExistent", null])]
public void GetProperty_TextBox_ReturnsValue(string attributeName, string expectedValue)
{
var driverOptions = FlaUIDriverOptions.TestApp();
using var driver = new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions);
var element = driver.FindElement(ExtendedBy.AccessibilityId("TextBox"));

var value = element.GetDomProperty(attributeName);

Assert.That(value, Is.EqualTo(expectedValue));
}

[Test]
public void GetAttribute_DesktopElement_ReturnsAttribute()
{
Expand Down Expand Up @@ -395,5 +409,23 @@ public void GetAttribute_PatternProperty_ReturnsValue()

Assert.That(value, Is.EqualTo("On"));
}

[Test]
public void GetProperty_PatternProperty_ReturnsValue()
{
var driverOptions = FlaUIDriverOptions.TestApp();
using var driver = new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions);
var element = driver.FindElement(ExtendedBy.AccessibilityId("SimpleCheckBox"));

var value = element.GetDomProperty("Toggle.ToggleState");

Assert.That(value, Is.EqualTo("Off"));

element.Click();

value = element.GetDomProperty("Toggle.ToggleState");

Assert.That(value, Is.EqualTo("On"));
}
}
}
32 changes: 32 additions & 0 deletions src/FlaUI.WebDriver.UITests/ExecuteTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using FlaUI.WebDriver.UITests.TestUtil;
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium.Remote;
using System.Collections.Generic;

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

[Test]
public void ExecuteScript_WindowsGetClipboard_IsSupported()
{
var driverOptions = FlaUIDriverOptions.TestApp();
using var driver = new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions);
var element = driver.FindElement(ExtendedBy.AccessibilityId("TextBox"));
element.Click();
new Actions(driver).KeyDown(Keys.Control).SendKeys("a").KeyUp(Keys.Control).Perform();
new Actions(driver).KeyDown(Keys.Control).SendKeys("c").KeyUp(Keys.Control).Perform();

var result = driver.ExecuteScript("windows: getClipboard", new Dictionary<string, object> {});

Assert.That(result, Is.EqualTo("Test TextBox"));
}

[Test]
public void ExecuteScript_WindowsSetClipboard_IsSupported()
{
var driverOptions = FlaUIDriverOptions.TestApp();
using var driver = new RemoteWebDriver(WebDriverFixture.WebDriverUrl, driverOptions);

var result = driver.ExecuteScript("windows: setClipboard", new Dictionary<string, object> {
["b64Content"] = "Pasted!"});

var element = driver.FindElement(ExtendedBy.AccessibilityId("TextBox"));
element.Click();
new Actions(driver).KeyDown(Keys.Control).SendKeys("v").KeyUp(Keys.Control).Perform();
Assert.That(element.Text, Is.EqualTo("Test TextBoxPasted!"));
}

[Test]
public void ExecuteScript_WindowsHoverXY_IsSupported()
{
Expand Down
1 change: 1 addition & 0 deletions src/FlaUI.WebDriver/Controllers/ElementController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ public async Task<ActionResult> ElementSendKeys([FromRoute] string sessionId, [F
}

[HttpGet("{elementId}/attribute/{attributeId}")]
[HttpGet("{elementId}/property/{attributeId}")]
public async Task<ActionResult> GetAttribute([FromRoute] string sessionId, [FromRoute] string elementId, [FromRoute] string attributeId)
{
var session = GetSession(sessionId);
Expand Down
34 changes: 34 additions & 0 deletions src/FlaUI.WebDriver/Controllers/ExecuteController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public async Task<ActionResult> ExecuteScript([FromRoute] string sessionId, [Fro
return await ExecuteWindowsHoverScript(session, executeScriptRequest);
case "windows: scroll":
return await ExecuteWindowsScrollScript(session, executeScriptRequest);
case "windows: setClipboard":
return await ExecuteWindowsSetClipboardScript(session, executeScriptRequest);
case "windows: getClipboard":
return await ExecuteWindowsGetClipboardScript(session, executeScriptRequest);
default:
throw WebDriverResponseException.UnsupportedOperation("Only 'powerShell', 'windows: keys', 'windows: click', 'windows: hover' scripts are supported");
}
Expand Down Expand Up @@ -90,6 +94,36 @@ private async Task<ActionResult> ExecutePowerShellScript(Session session, Execut
return WebDriverResult.Success(result);
}

private async Task<ActionResult> ExecuteWindowsSetClipboardScript(Session session, ExecuteScriptRequest executeScriptRequest)
{
if (executeScriptRequest.Args.Count != 1)
{
throw WebDriverResponseException.InvalidArgument($"Expected an array of exactly 1 arguments for the windows: setClipboard script, but got {executeScriptRequest.Args.Count} arguments");
}
var action = JsonSerializer.Deserialize<WindowsSetClipboardScript>(executeScriptRequest.Args[0], new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
if (action == null)
{
throw WebDriverResponseException.InvalidArgument("Action cannot be null");
}
await _windowsExtensionService.ExecuteSetClipboardScript(session, action);
return WebDriverResult.Success();
}

private async Task<ActionResult> ExecuteWindowsGetClipboardScript(Session session, ExecuteScriptRequest executeScriptRequest)
{
if (executeScriptRequest.Args.Count != 1)
{
throw WebDriverResponseException.InvalidArgument($"Expected an array of exactly 1 arguments for the windows: getClipboard script, but got {executeScriptRequest.Args.Count} arguments");
}
var action = JsonSerializer.Deserialize<WindowsGetClipboardScript>(executeScriptRequest.Args[0], new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
if (action == null)
{
throw WebDriverResponseException.InvalidArgument("Action cannot be null");
}
var result = await _windowsExtensionService.ExecuteGetClipboardScript(session, action);
return WebDriverResult.Success(result);
}

private async Task<ActionResult> ExecuteWindowsClickScript(Session session, ExecuteScriptRequest executeScriptRequest)
{
if (executeScriptRequest.Args.Count != 1)
Expand Down
7 changes: 7 additions & 0 deletions src/FlaUI.WebDriver/Models/WindowsGetClipboardScript.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace FlaUI.WebDriver.Models
{
public class WindowsGetClipboardScript
{
public string? ContentType { get; set; }
}
}
8 changes: 8 additions & 0 deletions src/FlaUI.WebDriver/Models/WindowsSetClipboardScript.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace FlaUI.WebDriver.Models
{
public class WindowsSetClipboardScript
{
public string B64Content { get; set; } = "";
public string? ContentType { get; set; }
}
}
2 changes: 2 additions & 0 deletions src/FlaUI.WebDriver/Services/IWindowsExtensionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ public interface IWindowsExtensionService
Task ExecuteScrollScript(Session session, WindowsScrollScript action);
Task ExecuteHoverScript(Session session, WindowsHoverScript action);
Task ExecuteKeyScript(Session session, WindowsKeyScript action);
Task<string> ExecuteGetClipboardScript(Session session, WindowsGetClipboardScript action);
Task ExecuteSetClipboardScript(Session session, WindowsSetClipboardScript action);
}
}
64 changes: 64 additions & 0 deletions src/FlaUI.WebDriver/Services/WindowsExtensionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,70 @@ public WindowsExtensionService(ILogger<WindowsExtensionService> logger)
_logger = logger;
}

public Task<string> ExecuteGetClipboardScript(Session session, WindowsGetClipboardScript action)
{
switch(action.ContentType)
{
default:
case "plaintext":
return Task.FromResult(ExecuteOnClipboardThread(
() => System.Windows.Forms.Clipboard.GetText(System.Windows.Forms.TextDataFormat.UnicodeText)
));
case "image":
return Task.FromResult(ExecuteOnClipboardThread(() =>
{
using var image = System.Windows.Forms.Clipboard.GetImage();
if (image == null)
{
return "";
}
using var stream = new MemoryStream();
image.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
return Convert.ToBase64String(stream.ToArray());
}));
}
}

public Task ExecuteSetClipboardScript(Session session, WindowsSetClipboardScript action)
{
switch (action.ContentType)
{
default:
case "plaintext":
ExecuteOnClipboardThread(() => System.Windows.Forms.Clipboard.SetText(action.B64Content));
break;
case "image":
ExecuteOnClipboardThread(() =>
{
using var stream = new MemoryStream(Convert.FromBase64String(action.B64Content));
using var image = Image.FromStream(stream);
System.Windows.Forms.Clipboard.SetImage(image);
});
break;
}
return Task.CompletedTask;
}

private void ExecuteOnClipboardThread(System.Action action)
{
// See https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.clipboard?view=windowsdesktop-8.0#remarks
var thread = new Thread(() => action());
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
}

private string ExecuteOnClipboardThread(Func<string> method)
{
// See https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.clipboard?view=windowsdesktop-8.0#remarks
string result = "";
var thread = new Thread(() => { result = method(); });
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
return result;
}

public async Task ExecuteClickScript(Session session, WindowsClickScript action)
{
if (action.DurationMs.HasValue)
Expand Down

0 comments on commit 70a249b

Please sign in to comment.