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)