From b7755a8e9f7ac618ffcac33646c4515b8d82525d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C4=81rti=C5=86=C5=A1=20Kristaps=20Mickus?= <64271878+MartinsKMickus@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:03:48 +0200 Subject: [PATCH 1/6] Remote Windows Appium support automation with `appiumUrl` --- README.md | 4 +- examples/tests/cases/case_windows_tests.yaml | 33 +++++++++++++ examples/tests/cases/config.yaml | 11 +++++ lib/core/device_drivers.rb | 51 ++++++++++++-------- 4 files changed, 78 insertions(+), 21 deletions(-) create mode 100644 examples/tests/cases/case_windows_tests.yaml diff --git a/README.md b/README.md index 18b23deb..6fddcd4c 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,9 @@ Apps: WinPath: C:\Users\user\AppData\Local\Programs\SomeApp\SomeApp.exe UWPAppName: SOMEAPP.1234567890ABC_defghijklmnop!App MacAppName: com.someapp - + +> [!NOTE] +> If `WinPath` is used it assumes that app runs on the same machine where test is launched. To use with `appiumUrl` you may define app path under Appium capabilities within config file This will add all the necessary capabilities to run on iOS, MacOS, Windows and Android diff --git a/examples/tests/cases/case_windows_tests.yaml b/examples/tests/cases/case_windows_tests.yaml new file mode 100644 index 00000000..af4f0c54 --- /dev/null +++ b/examples/tests/cases/case_windows_tests.yaml @@ -0,0 +1,33 @@ +RemoteWindowsTestExample: + Roles: + - Role: remoteWindows + App: RootWindows + Actions: + - Type: case + Value: WindowsTestExample + Role: remoteWindows + +LocalWindowsTestExample: + Roles: + - Role: localWindows + App: RootWindows + Actions: + - Type: case + Value: WindowsTestExample + Role: localWindows + +WindowsTestExample: + Actions: + - Type: screenshot + Name: windows_home_screen + - Type: click + Strategy: xpath + Id: "//Text[@Name=\"Search\"]" + - Type: send_keys + Strategy: xpath + Id: "//Edit[@Name=\"Search box\"]" + Value: Settings + - Type: sleep + Time: 2 + - Type: screenshot + Name: windows_search_done \ No newline at end of file diff --git a/examples/tests/cases/config.yaml b/examples/tests/cases/config.yaml index a0333fb6..01a3ddb0 100644 --- a/examples/tests/cases/config.yaml +++ b/examples/tests/cases/config.yaml @@ -6,6 +6,8 @@ Apps: Activity: com.android.vending.AssetBrowserActivity Settings: iOSBundle: com.apple.Preferences + RootWindows: + WinPath: Root # Not required for appiumURL but must be present for YAML validity chromeDriverPath: chromedriver @@ -29,6 +31,15 @@ Devices: # IOS - role: localiOS platform: iOS + # WINDOWS + - role: remoteWindows + platform: Windows + appiumUrl: http://IP:PORT # Windows device with running Appium server + capabilities: + automationName: Windows + app: Root # Root windows allows to automate everything on windows screen + - role: localWindows + platform: Windows #VARS diff --git a/lib/core/device_drivers.rb b/lib/core/device_drivers.rb index 71d7f74e..e0d4490e 100644 --- a/lib/core/device_drivers.rb +++ b/lib/core/device_drivers.rb @@ -69,29 +69,40 @@ def build_mac_caps # assemble basic capabilities for Windows def build_windows_caps - process_check = execute_powershell("Get-Process #{@app}") - if process_check.include? "Exception" - if @app_details.key?("UWPAppName") # launch UWP app - spawn("start shell:AppsFolder\\#{@app_details["UWPAppName"]}") - else # launch Win32 app - spawn(execute_powershell("where.exe /r $HOME #{@app}.exe")) - end - sleep(5) - end - - processWindowHandles = execute_powershell("(Get-Process #{@app}).MainWindowHandle").split("\n") - appMainWindowHandleList = (processWindowHandles.select { |wh| wh.to_i != 0 }) - hexMainWindowHandle = appMainWindowHandleList[-1].to_i.to_s(16) - caps = { - "platformName" => "Windows", - "forceMjsonwp" => true, - "newCommandTimeout" => 2000 * 60, + 'platformName' => 'Windows' } + if @app_details.key?("WinPath") - caps.merge!({ "app" => @app_details["WinPath"] }) - else - caps.merge!({ "appTopLevelWindow" => "#{hexMainWindowHandle}" }) + path_to_executable = @app_details["WinPath"] + caps.merge!({ "app" => "#{path_to_executable}" }) + return caps + end + + if @url.nil? # Local driver + if !OS.windows? + log_abort("Cannot run a local windows role on a non-windows operating system!") + else + process_check = execute_powershell("Get-Process #{@app}") + if process_check.include? "Exception" + if @app_details.key?("UWPAppName") # launch UWP app + spawn("start shell:AppsFolder\\#{@app_details["UWPAppName"]}") + else # launch Win32 app + spawn(execute_powershell("where.exe /r $HOME #{@app}.exe")) + end + sleep(5) + end + processWindowHandles = execute_powershell("(Get-Process #{@app}).MainWindowHandle").split("\n") + appMainWindowHandleList = (processWindowHandles.select { |wh| wh.to_i != 0 }) + hexMainWindowHandle = appMainWindowHandleList[-1].to_i.to_s(16) + if @app_details.key?("WinPath") + caps.merge!({ "app" => @app_details["WinPath"] }) + else + caps.merge!({ "appTopLevelWindow" => "#{hexMainWindowHandle.to_s}" }) + end + end + else # Remote driver + log_warn('Neither WinPath or MainWindowHandle is specified! Make sure config file has correct Appium capabilities.') end return caps end From ae7e2ab8fded7b615fe4095d2c88b6c5220f6734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C4=81rti=C5=86=C5=A1=20Kristaps=20Mickus?= <64271878+MartinsKMickus@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:03:57 +0200 Subject: [PATCH 2/6] Fixes Appium server launch if spaces in log path --- lib/core/appium_server.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/core/appium_server.rb b/lib/core/appium_server.rb index 24127de7..5d8846cb 100644 --- a/lib/core/appium_server.rb +++ b/lib/core/appium_server.rb @@ -20,8 +20,9 @@ def start @port = @port + 2 opened = `netstat -anp tcp | #{grep_cmd} "#{@port}"`.include?("LISTEN") end + log_debug("Executing: appium --base-path=/wd/hub -p #{@port} >> \"#{folder}/#{@udid}.log\" 2>&1") - spawn("appium --base-path=/wd/hub -p #{@port} >> #{folder}/#{@udid}.log 2>&1") + spawn("appium --base-path=/wd/hub -p #{@port} >> \"#{folder}/#{@udid}.log\" 2>&1") opened = false log_info("Role '#{@role}': Starting Appium server on port #{@port} ", no_date=false, _print=true) From 5787d5b862ff822a4cc0f0e5b2e07d310a30e4af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C4=81rti=C5=86=C5=A1=20Kristaps=20Mickus?= <64271878+MartinsKMickus@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:04:01 +0200 Subject: [PATCH 3/6] Adds automationName capability in setup Now it is no more required to set automationName in config or local Windows tests. --- lib/core/device_drivers.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/core/device_drivers.rb b/lib/core/device_drivers.rb index e0d4490e..c42bd33f 100644 --- a/lib/core/device_drivers.rb +++ b/lib/core/device_drivers.rb @@ -70,7 +70,8 @@ def build_mac_caps # assemble basic capabilities for Windows def build_windows_caps caps = { - 'platformName' => 'Windows' + 'platformName' => 'Windows', + 'automationName' => 'Windows' } if @app_details.key?("WinPath") From eab284547a7de2b8b18032f1e61d52fa8c7715af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C4=81rti=C5=86=C5=A1=20Kristaps=20Mickus?= <64271878+MartinsKMickus@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:09:37 +0200 Subject: [PATCH 4/6] Improved log message for remote Appium Win --- lib/core/device_drivers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/device_drivers.rb b/lib/core/device_drivers.rb index c42bd33f..25c736c4 100644 --- a/lib/core/device_drivers.rb +++ b/lib/core/device_drivers.rb @@ -103,7 +103,7 @@ def build_windows_caps end end else # Remote driver - log_warn('Neither WinPath or MainWindowHandle is specified! Make sure config file has correct Appium capabilities.') + log_info('Remote Appium Windows device mode') end return caps end From 1b790aac8975827daa40bd1b5e73649a4b402cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C4=81rti=C5=86=C5=A1=20Kristaps=20Mickus?= <64271878+MartinsKMickus@users.noreply.github.com> Date: Tue, 6 Jan 2026 14:32:35 +0200 Subject: [PATCH 5/6] Win capability building improvement Improves logic on Windows capabilities while providing localhost vs 127.0.0.1 setting difference. Fixes cases when path to Windows app contains spaces. Improved debug while starting local Windows app. Enables both local/remote Windows automation while just requiring appiumUrl to be set. Updated README about Win automation. --- README.md | 4 +-- examples/tests/cases/config.yaml | 5 +-- lib/core/device_drivers.rb | 59 +++++++++++++++++--------------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 6fddcd4c..44f3c406 100644 --- a/README.md +++ b/README.md @@ -107,8 +107,6 @@ Apps: UWPAppName: SOMEAPP.1234567890ABC_defghijklmnop!App MacAppName: com.someapp -> [!NOTE] -> If `WinPath` is used it assumes that app runs on the same machine where test is launched. To use with `appiumUrl` you may define app path under Appium capabilities within config file This will add all the necessary capabilities to run on iOS, MacOS, Windows and Android @@ -169,6 +167,8 @@ Devices: - role: localWindows platform: Windows +> [!NOTE] +> To control all Windows screen elemets you may set `WinPath: Root` under app config. If `WinPath` is not defined and you use `appiumUrl: http://127.0.0.1:PORT` under Windows role you may configure app or hexMainWindowHandle yourself under Windows role below `capabilities:` key, if using `http://localhost:PORT` it will auto search for windows handle (on local Win run). As default (local Win) app to be launched is automatically searched as exe file within Windows home directory which name matches app key name within config. ## Create Test Case diff --git a/examples/tests/cases/config.yaml b/examples/tests/cases/config.yaml index 01a3ddb0..744654f4 100644 --- a/examples/tests/cases/config.yaml +++ b/examples/tests/cases/config.yaml @@ -7,7 +7,7 @@ Apps: Settings: iOSBundle: com.apple.Preferences RootWindows: - WinPath: Root # Not required for appiumURL but must be present for YAML validity + WinPath: Root # Controls everything on screen chromeDriverPath: chromedriver @@ -35,9 +35,6 @@ Devices: - role: remoteWindows platform: Windows appiumUrl: http://IP:PORT # Windows device with running Appium server - capabilities: - automationName: Windows - app: Root # Root windows allows to automate everything on windows screen - role: localWindows platform: Windows diff --git a/lib/core/device_drivers.rb b/lib/core/device_drivers.rb index 25c736c4..e56fe5d8 100644 --- a/lib/core/device_drivers.rb +++ b/lib/core/device_drivers.rb @@ -70,41 +70,44 @@ def build_mac_caps # assemble basic capabilities for Windows def build_windows_caps caps = { - 'platformName' => 'Windows', - 'automationName' => 'Windows' + "platformName" => "Windows", + "automationName" => "Windows", + "newCommandTimeout" => 2000 * 60, } - if @app_details.key?("WinPath") - path_to_executable = @app_details["WinPath"] - caps.merge!({ "app" => "#{path_to_executable}" }) + caps.merge!({ "app" => @app_details["WinPath"] }) + return caps # No matter if localhost or remote, because Appium will determine window handle automatically + end + + unless @url.include? "localhost" + # Either remote or no need to check for windows handle (127.0.0.1) return caps end - if @url.nil? # Local driver - if !OS.windows? - log_abort("Cannot run a local windows role on a non-windows operating system!") - else - process_check = execute_powershell("Get-Process #{@app}") - if process_check.include? "Exception" - if @app_details.key?("UWPAppName") # launch UWP app - spawn("start shell:AppsFolder\\#{@app_details["UWPAppName"]}") - else # launch Win32 app - spawn(execute_powershell("where.exe /r $HOME #{@app}.exe")) - end - sleep(5) - end - processWindowHandles = execute_powershell("(Get-Process #{@app}).MainWindowHandle").split("\n") - appMainWindowHandleList = (processWindowHandles.select { |wh| wh.to_i != 0 }) - hexMainWindowHandle = appMainWindowHandleList[-1].to_i.to_s(16) - if @app_details.key?("WinPath") - caps.merge!({ "app" => @app_details["WinPath"] }) - else - caps.merge!({ "appTopLevelWindow" => "#{hexMainWindowHandle.to_s}" }) - end + # Running on "localhost" with automatic path to app detection in home directory + if !OS.windows? + log_abort("Cannot run a local windows role on a non-windows operating system!") + end + process_check = execute_powershell("Get-Process \"#{@app}\"") + if process_check.include? "Exception" + if @app_details.key?("UWPAppName") # launch UWP app + spawn("start shell:AppsFolder\\#{@app_details["UWPAppName"]}") + else # launch Win32 app + app_path = execute_powershell("where.exe /r $HOME \"#{@app}.exe\"").strip + log_debug("Found app path: #{app_path}") + # Array syntax allows to handle any spaces in the filepath. + pid = spawn([app_path, app_path]) + Process.detach(pid) end - else # Remote driver - log_info('Remote Appium Windows device mode') + sleep(5) end + + # Note: Capabilities app and appTopLevelWindow cannot work together) + processWindowHandles = execute_powershell("(Get-Process \"#{@app}\").MainWindowHandle").split("\n") + appMainWindowHandleList = (processWindowHandles.select { |wh| wh.to_i != 0 }) + hexMainWindowHandle = appMainWindowHandleList[-1].to_i.to_s(16) + caps.merge!({ "appTopLevelWindow" => "#{hexMainWindowHandle}" }) + return caps end From d0d70ff472f440d71cb2b82f3778eb1f8df76714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C4=81rti=C5=86=C5=A1=20Kristaps=20Mickus?= <64271878+MartinsKMickus@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:05:19 +0200 Subject: [PATCH 6/6] Improve Windows app configuration instructions Clarified instructions for setting WinPath and appiumUrl for Windows roles. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 44f3c406..4acf82a8 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,8 @@ Devices: platform: Windows > [!NOTE] -> To control all Windows screen elemets you may set `WinPath: Root` under app config. If `WinPath` is not defined and you use `appiumUrl: http://127.0.0.1:PORT` under Windows role you may configure app or hexMainWindowHandle yourself under Windows role below `capabilities:` key, if using `http://localhost:PORT` it will auto search for windows handle (on local Win run). As default (local Win) app to be launched is automatically searched as exe file within Windows home directory which name matches app key name within config. +> For Windows apps, you can set `WinPath` under the app config to `Root` in order to control all Windows screen elements. +If `WinPath` is not provided and `appiumUrl` is either absent or set to `localhost`, the framework will automatically attempt to search for the application window handle on the local machine. You can skip this behavior by setting `appiumUrl` to `127.0.0.1`, which will allow you to manually set the app or `hexMainWindowHandle` inside the role's `capabilities`. ## Create Test Case