From d1650bd1cadb47a1770c49ad10f0d73c7f28ed36 Mon Sep 17 00:00:00 2001 From: aristotelos Date: Wed, 20 Nov 2024 17:00:41 +0100 Subject: [PATCH 1/2] Add support for element property Treat element attributes and properties as equal, just like appium-windows-driver. --- README.md | 2 +- src/FlaUI.WebDriver.UITests/ElementTests.cs | 32 +++++++++++++++++++ .../Controllers/ElementController.cs | 1 + 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f10bd76..4336114 100644 --- a/README.md +++ b/README.md @@ -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: | diff --git a/src/FlaUI.WebDriver.UITests/ElementTests.cs b/src/FlaUI.WebDriver.UITests/ElementTests.cs index 431c947..2f20192 100644 --- a/src/FlaUI.WebDriver.UITests/ElementTests.cs +++ b/src/FlaUI.WebDriver.UITests/ElementTests.cs @@ -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() { @@ -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")); + } } } diff --git a/src/FlaUI.WebDriver/Controllers/ElementController.cs b/src/FlaUI.WebDriver/Controllers/ElementController.cs index 41d358c..de6b836 100644 --- a/src/FlaUI.WebDriver/Controllers/ElementController.cs +++ b/src/FlaUI.WebDriver/Controllers/ElementController.cs @@ -229,6 +229,7 @@ public async Task ElementSendKeys([FromRoute] string sessionId, [F } [HttpGet("{elementId}/attribute/{attributeId}")] + [HttpGet("{elementId}/property/{attributeId}")] public async Task GetAttribute([FromRoute] string sessionId, [FromRoute] string elementId, [FromRoute] string attributeId) { var session = GetSession(sessionId); From 70265148e6574b8cc99f72e4c02e0231a35375fd Mon Sep 17 00:00:00 2001 From: aristotelos Date: Wed, 20 Nov 2024 17:02:48 +0100 Subject: [PATCH 2/2] Add getClipboard and setClipboard extensions Add extensions that are also supported by appium-windows-driver. --- README.md | 2 +- src/FlaUI.WebDriver.UITests/ExecuteTests.cs | 32 ++++++++++ .../Controllers/ExecuteController.cs | 34 ++++++++++ .../Models/WindowsGetClipboardScript.cs | 7 ++ .../Models/WindowsSetClipboardScript.cs | 8 +++ .../Services/IWindowsExtensionService.cs | 2 + .../Services/WindowsExtensionService.cs | 64 +++++++++++++++++++ 7 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/FlaUI.WebDriver/Models/WindowsGetClipboardScript.cs create mode 100644 src/FlaUI.WebDriver/Models/WindowsSetClipboardScript.cs diff --git a/README.md b/README.md index 4336114..824ee5c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/FlaUI.WebDriver.UITests/ExecuteTests.cs b/src/FlaUI.WebDriver.UITests/ExecuteTests.cs index 917b473..6380d81 100644 --- a/src/FlaUI.WebDriver.UITests/ExecuteTests.cs +++ b/src/FlaUI.WebDriver.UITests/ExecuteTests.cs @@ -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; @@ -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 {}); + + 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 { + ["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() { diff --git a/src/FlaUI.WebDriver/Controllers/ExecuteController.cs b/src/FlaUI.WebDriver/Controllers/ExecuteController.cs index 4502de0..c6d6858 100644 --- a/src/FlaUI.WebDriver/Controllers/ExecuteController.cs +++ b/src/FlaUI.WebDriver/Controllers/ExecuteController.cs @@ -37,6 +37,10 @@ public async Task 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"); } @@ -90,6 +94,36 @@ private async Task ExecutePowerShellScript(Session session, Execut return WebDriverResult.Success(result); } + private async Task 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(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 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(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 ExecuteWindowsClickScript(Session session, ExecuteScriptRequest executeScriptRequest) { if (executeScriptRequest.Args.Count != 1) diff --git a/src/FlaUI.WebDriver/Models/WindowsGetClipboardScript.cs b/src/FlaUI.WebDriver/Models/WindowsGetClipboardScript.cs new file mode 100644 index 0000000..286101a --- /dev/null +++ b/src/FlaUI.WebDriver/Models/WindowsGetClipboardScript.cs @@ -0,0 +1,7 @@ +namespace FlaUI.WebDriver.Models +{ + public class WindowsGetClipboardScript + { + public string? ContentType { get; set; } + } +} diff --git a/src/FlaUI.WebDriver/Models/WindowsSetClipboardScript.cs b/src/FlaUI.WebDriver/Models/WindowsSetClipboardScript.cs new file mode 100644 index 0000000..3aad501 --- /dev/null +++ b/src/FlaUI.WebDriver/Models/WindowsSetClipboardScript.cs @@ -0,0 +1,8 @@ +namespace FlaUI.WebDriver.Models +{ + public class WindowsSetClipboardScript + { + public string B64Content { get; set; } = ""; + public string? ContentType { get; set; } + } +} diff --git a/src/FlaUI.WebDriver/Services/IWindowsExtensionService.cs b/src/FlaUI.WebDriver/Services/IWindowsExtensionService.cs index 340b067..f2abe95 100644 --- a/src/FlaUI.WebDriver/Services/IWindowsExtensionService.cs +++ b/src/FlaUI.WebDriver/Services/IWindowsExtensionService.cs @@ -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 ExecuteGetClipboardScript(Session session, WindowsGetClipboardScript action); + Task ExecuteSetClipboardScript(Session session, WindowsSetClipboardScript action); } } \ No newline at end of file diff --git a/src/FlaUI.WebDriver/Services/WindowsExtensionService.cs b/src/FlaUI.WebDriver/Services/WindowsExtensionService.cs index d10ae20..b3067e4 100644 --- a/src/FlaUI.WebDriver/Services/WindowsExtensionService.cs +++ b/src/FlaUI.WebDriver/Services/WindowsExtensionService.cs @@ -15,6 +15,70 @@ public WindowsExtensionService(ILogger logger) _logger = logger; } + public Task 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 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)