From 8b29538143b6efe960f8020b60f14b0986ecbd4a Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 8 Aug 2025 10:21:54 +0900 Subject: [PATCH 01/13] feat: add BiDiBridge --- lib/appium_lib_core/common/base/bridge.rb | 12 +++++++++++- lib/appium_lib_core/common/base/driver.rb | 4 ++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/appium_lib_core/common/base/bridge.rb b/lib/appium_lib_core/common/base/bridge.rb index 18c04d1d..16e8e6eb 100644 --- a/lib/appium_lib_core/common/base/bridge.rb +++ b/lib/appium_lib_core/common/base/bridge.rb @@ -21,7 +21,9 @@ def convert(how, what) end end # LocatorConverter - class Bridge < ::Selenium::WebDriver::Remote::Bridge + # TODO: switch to use BiDiBridge with 'webSocketUrl' + class Bridge < ::Selenium::WebDriver::Remote::BiDiBridge + # class Bridge < ::Selenium::WebDriver::Remote::Bridge include Device::DeviceLock include Device::Keyboard include Device::ImeActions @@ -116,6 +118,9 @@ def create_session(capabilities) raise ::Selenium::WebDriver::Error::WebDriverError, 'no sessionId in returned payload' unless @session_id @capabilities = json_create(response['capabilities']) + + socket_url = @capabilities[:web_socket_url] + @bidi = ::Selenium::WebDriver::BiDi.new(url: socket_url) if socket_url end # Append +appium:+ prefix for Appium following W3C spec @@ -313,6 +318,11 @@ def element_screenshot(element_id) def send_command(command_params) execute :chrome_send_command, {}, command_params end + + def quit + bidi&.close + super + end end # class Bridge end # class Base end # module Core diff --git a/lib/appium_lib_core/common/base/driver.rb b/lib/appium_lib_core/common/base/driver.rb index b66e3648..12892fcb 100644 --- a/lib/appium_lib_core/common/base/driver.rb +++ b/lib/appium_lib_core/common/base/driver.rb @@ -29,6 +29,9 @@ class Driver < ::Selenium::WebDriver::Driver include ::Selenium::WebDriver::DriverExtensions::UploadsFiles include ::Selenium::WebDriver::DriverExtensions::HasSessionId + # TODO: allow to install only for newer version + include ::Selenium::WebDriver::DriverExtensions::HasBiDi + include ::Appium::Core::Base::Rotatable include ::Appium::Core::Base::TakesScreenshot include ::Appium::Core::Base::HasRemoteStatus @@ -54,6 +57,7 @@ def initialize(bridge: nil, listener: nil, **opts) # rubocop:disable Lint/Missin @devtools = nil @bidi = nil + # TODO: modify the eleemnt_class # in the selenium webdriver as well ::Selenium::WebDriver::Remote::Bridge.element_class = ::Appium::Core::Element bridge ||= create_bridge(**opts) From cdaf1bc0723371df52d62a23de36dcaee05f459b Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 8 Aug 2025 10:22:10 +0900 Subject: [PATCH 02/13] add bidi test --- .../functional/android/webdriver/bidi_test.rb | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 test/functional/android/webdriver/bidi_test.rb diff --git a/test/functional/android/webdriver/bidi_test.rb b/test/functional/android/webdriver/bidi_test.rb new file mode 100644 index 00000000..73b542cb --- /dev/null +++ b/test/functional/android/webdriver/bidi_test.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'test_helper' + +# $ rake test:func:android TEST=test/functional/android/webdriver/create_session_test.rb +class AppiumLibCoreTest + module WebDriver + class BidiTest < AppiumLibCoreTest::Function::TestCase + def test_bidi + caps = Caps.android + caps[:capabilities]['webSocketUrl'] = true + core = ::Appium::Core.for(caps) + + driver = core.start_driver + assert !driver.capabilities.nil? + + log_entries = [] + + driver.bidi.send_cmd('session.subscribe', 'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP']) + subscribe_id = driver.bidi.add_callback('log.entryAdded') do |params| + log_entries << params + end + + driver.page_source + + driver.bidi.remove_callback('log.entryAdded', subscribe_id) + driver.bidi.send_cmd('session.unsubscribe', 'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP']) + + driver&.quit + end + end + end +end From 835d1f0c774781a69cbf91f087cc8bfb71bde1bb Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 8 Aug 2025 10:23:14 +0900 Subject: [PATCH 03/13] add tests --- .github/workflows/functional-test.yml | 4 ++-- test/functional/android/webdriver/bidi_test.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index 7d86a98a..df3501b4 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -199,10 +199,10 @@ jobs: - target: test/functional/android/driver_test.rb,test/functional/android/patch_test.rb,test/functional/android/android/device_test.rb,test/functional/android/android/search_context_test.rb automation_name: espresso name: test2 - - target: test/functional/android/webdriver/create_session_test.rb,test/functional/android/webdriver/device_test.rb,test/functional/android/webdriver/w3c_actions_test.rb + - target: test/functional/android/webdriver/create_session_test.rb,test/functional/android/webdriver/device_test.rb,test/functional/android/webdriver/w3c_actions_test.rb,test/functional/android/webdriver/bidi_test.rb automation_name: uiautomator2 name: test3 - - target: test/functional/android/webdriver/create_session_test.rb,test/functional/android/webdriver/device_test.rb,test/functional/android/webdriver/w3c_actions_test.rb + - target: test/functional/android/webdriver/create_session_test.rb,test/functional/android/webdriver/device_test.rb,test/functional/android/webdriver/w3c_actions_test.rb,test/functional/android/webdriver/bidi_test.rb automation_name: espresso name: test4 - target: test/functional/android/android/mobile_commands_test.rb diff --git a/test/functional/android/webdriver/bidi_test.rb b/test/functional/android/webdriver/bidi_test.rb index 73b542cb..b0cf7b87 100644 --- a/test/functional/android/webdriver/bidi_test.rb +++ b/test/functional/android/webdriver/bidi_test.rb @@ -14,7 +14,7 @@ require 'test_helper' -# $ rake test:func:android TEST=test/functional/android/webdriver/create_session_test.rb +# $ rake test:func:android TEST=test/functional/android/webdriver/bidi_test.rb class AppiumLibCoreTest module WebDriver class BidiTest < AppiumLibCoreTest::Function::TestCase From 30be2529d87c0541f5f6deea94804e2c59e1b6cf Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 8 Aug 2025 11:27:19 +0900 Subject: [PATCH 04/13] add bdiege class --- lib/appium_lib_core/common/base.rb | 3 +- .../common/base/bidi_bridge.rb | 87 +++++++++++++++++++ lib/appium_lib_core/common/base/bridge.rb | 9 +- lib/appium_lib_core/common/base/driver.rb | 25 ++++-- 4 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 lib/appium_lib_core/common/base/bidi_bridge.rb diff --git a/lib/appium_lib_core/common/base.rb b/lib/appium_lib_core/common/base.rb index 490fb722..3738565c 100644 --- a/lib/appium_lib_core/common/base.rb +++ b/lib/appium_lib_core/common/base.rb @@ -30,8 +30,9 @@ require_relative 'device/orientation' # The following files have selenium-webdriver related stuff. -require_relative 'base/driver' require_relative 'base/bridge' +require_relative 'base/bidi_bridge' +require_relative 'base/driver' require_relative 'base/capabilities' require_relative 'base/http_default' require_relative 'base/search_context' diff --git a/lib/appium_lib_core/common/base/bidi_bridge.rb b/lib/appium_lib_core/common/base/bidi_bridge.rb new file mode 100644 index 00000000..b46ab1bd --- /dev/null +++ b/lib/appium_lib_core/common/base/bidi_bridge.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require_relative 'bridge' + +module Appium + module Core + class Base + class BiDiBridge < ::Appium::Core::Base::Bridge + attr_reader :bidi + + # Override + # Creates session handling. + # + # @param [::Appium::Core::Base::Capabilities, Hash] capabilities A capability + # @return [::Appium::Core::Base::Capabilities] + # + # @example + # + # opts = { + # caps: { + # platformName: :ios, + # automationName: 'XCUITest', + # app: 'test/functional/app/UICatalog.app.zip', + # platformVersion: '11.4', + # deviceName: 'iPhone Simulator', + # useNewWDA: true, + # }, + # appium_lib: { + # wait: 30 + # } + # } + # core = ::Appium::Core.for(caps) + # driver = core.start_driver + # + def create_session(capabilities) + super + socket_url = @capabilities[:web_socket_url] + @bidi = ::Selenium::WebDriver::BiDi.new(url: socket_url) + end + + def get(url) + browsing_context.navigate(url) + end + + def go_back + browsing_context.traverse_history(-1) + end + + def go_forward + browsing_context.traverse_history(1) + end + + def refresh + browsing_context.reload + end + + def quit + super + ensure + bidi.close + end + + def close + execute(:close_window).tap { |handles| bidi.close if handles.empty? } + end + + private + + def browsing_context + @browsing_context ||= ::Selenium::WebDriver::BiDi::BrowsingContext.new(self) + end + end # class BiDiBridge + end # class Base + end # module Core +end # module Appium diff --git a/lib/appium_lib_core/common/base/bridge.rb b/lib/appium_lib_core/common/base/bridge.rb index 16e8e6eb..61b10980 100644 --- a/lib/appium_lib_core/common/base/bridge.rb +++ b/lib/appium_lib_core/common/base/bridge.rb @@ -21,9 +21,7 @@ def convert(how, what) end end # LocatorConverter - # TODO: switch to use BiDiBridge with 'webSocketUrl' - class Bridge < ::Selenium::WebDriver::Remote::BiDiBridge - # class Bridge < ::Selenium::WebDriver::Remote::Bridge + class Bridge < ::Selenium::WebDriver::Remote::Bridge include Device::DeviceLock include Device::Keyboard include Device::ImeActions @@ -318,11 +316,6 @@ def element_screenshot(element_id) def send_command(command_params) execute :chrome_send_command, {}, command_params end - - def quit - bidi&.close - super - end end # class Bridge end # class Base end # module Core diff --git a/lib/appium_lib_core/common/base/driver.rb b/lib/appium_lib_core/common/base/driver.rb index 12892fcb..2865b3de 100644 --- a/lib/appium_lib_core/common/base/driver.rb +++ b/lib/appium_lib_core/common/base/driver.rb @@ -29,9 +29,6 @@ class Driver < ::Selenium::WebDriver::Driver include ::Selenium::WebDriver::DriverExtensions::UploadsFiles include ::Selenium::WebDriver::DriverExtensions::HasSessionId - # TODO: allow to install only for newer version - include ::Selenium::WebDriver::DriverExtensions::HasBiDi - include ::Appium::Core::Base::Rotatable include ::Appium::Core::Base::TakesScreenshot include ::Appium::Core::Base::HasRemoteStatus @@ -57,9 +54,9 @@ def initialize(bridge: nil, listener: nil, **opts) # rubocop:disable Lint/Missin @devtools = nil @bidi = nil - # TODO: modify the eleemnt_class - # in the selenium webdriver as well - ::Selenium::WebDriver::Remote::Bridge.element_class = ::Appium::Core::Element + # internal use + @has_bidi = false + bridge ||= create_bridge(**opts) add_extensions(bridge.browser) @bridge = listener ? ::Appium::Support::EventFiringBridge.new(bridge, listener, **original_opts) : bridge @@ -83,7 +80,14 @@ def create_bridge(**opts) raise ::Appium::Core::Error::ArgumentError, "Unable to create a driver with parameters: #{opts}" unless opts.empty? - bridge = ::Appium::Core::Base::Bridge.new(**bridge_opts) + if capabilities['webSocketUrl'] + @has_bidi = true + ::Selenium::WebDriver::Remote::BiDiBridge.element_class = ::Appium::Core::Element + bridge = ::Appium::Core::Base::BiDiBridge.new(**bridge_opts) + else + ::Selenium::WebDriver::Remote::Bridge.element_class = ::Appium::Core::Element + bridge = ::Appium::Core::Base::Bridge.new(**bridge_opts) + end if session_id.nil? bridge.create_session(capabilities) @@ -97,6 +101,13 @@ def create_bridge(**opts) public + def bidi + return @bridge.bidi if @has_bidi + + msg = 'BiDi must be enabled by providing webSocketUrl capability to true' + raise(::Selenium::WebDriver::Error::WebDriverError, msg) + end + # Update +server_url+ and HTTP clients following this arguments, protocol, host, port and path. # After this method, +@bridge.http+ will be a new instance following them instead of +server_url+ which is # set before creating session. From d97e743e29942ab8938e47b1be3cc8216ac8b01b Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 8 Aug 2025 11:29:18 +0900 Subject: [PATCH 05/13] add webSocketUrl --- lib/appium_lib_core/common/base/bidi_bridge.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/appium_lib_core/common/base/bidi_bridge.rb b/lib/appium_lib_core/common/base/bidi_bridge.rb index b46ab1bd..87a38f22 100644 --- a/lib/appium_lib_core/common/base/bidi_bridge.rb +++ b/lib/appium_lib_core/common/base/bidi_bridge.rb @@ -30,12 +30,11 @@ class BiDiBridge < ::Appium::Core::Base::Bridge # # opts = { # caps: { - # platformName: :ios, - # automationName: 'XCUITest', - # app: 'test/functional/app/UICatalog.app.zip', - # platformVersion: '11.4', - # deviceName: 'iPhone Simulator', - # useNewWDA: true, + # platformName: :android, + # automationName: 'uiautomator2', + # platformVersion: '15', + # deviceName: 'Android', + #. webSocketUrl: true, # }, # appium_lib: { # wait: 30 From b3900f7b69cef08851eaa0abdc45d9ab73dc9c22 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 8 Aug 2025 11:32:28 +0900 Subject: [PATCH 06/13] add docstring --- lib/appium_lib_core/common/base/driver.rb | 29 +++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/appium_lib_core/common/base/driver.rb b/lib/appium_lib_core/common/base/driver.rb index 2865b3de..b48ae3b7 100644 --- a/lib/appium_lib_core/common/base/driver.rb +++ b/lib/appium_lib_core/common/base/driver.rb @@ -101,13 +101,6 @@ def create_bridge(**opts) public - def bidi - return @bridge.bidi if @has_bidi - - msg = 'BiDi must be enabled by providing webSocketUrl capability to true' - raise(::Selenium::WebDriver::Error::WebDriverError, msg) - end - # Update +server_url+ and HTTP clients following this arguments, protocol, host, port and path. # After this method, +@bridge.http+ will be a new instance following them instead of +server_url+ which is # set before creating session. @@ -1011,6 +1004,28 @@ def execute_driver(script: '', type: 'webdriverio', timeout_ms: nil) def convert_to_element(response_id) @bridge.convert_to_element response_id end + + # Return bidi instance + # @return [::Selenium::WebDriver::BiDi] + # + # @example + # + # log_entries = [] + # driver.bidi.send_cmd('session.subscribe', 'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP']) + # subscribe_id = driver.bidi.add_callback('log.entryAdded') do |params| + # log_entries << params + # end + # driver.page_source + # + # driver.bidi.remove_callback('log.entryAdded', subscribe_id) + # driver.bidi.send_cmd('session.unsubscribe', 'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP']) + # + def bidi + return @bridge.bidi if @has_bidi + + msg = 'BiDi must be enabled by providing webSocketUrl capability to true' + raise(::Selenium::WebDriver::Error::WebDriverError, msg) + end end # class Driver end # class Base end # module Core From 7f21518805bb0b1c55f67b237aadb166c8da0c7e Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 8 Aug 2025 11:33:50 +0900 Subject: [PATCH 07/13] fix rubocop --- lib/appium_lib_core/common/base/bidi_bridge.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/appium_lib_core/common/base/bidi_bridge.rb b/lib/appium_lib_core/common/base/bidi_bridge.rb index 87a38f22..420d7aea 100644 --- a/lib/appium_lib_core/common/base/bidi_bridge.rb +++ b/lib/appium_lib_core/common/base/bidi_bridge.rb @@ -34,7 +34,7 @@ class BiDiBridge < ::Appium::Core::Base::Bridge # automationName: 'uiautomator2', # platformVersion: '15', # deviceName: 'Android', - #. webSocketUrl: true, + # webSocketUrl: true, # }, # appium_lib: { # wait: 30 From b45474be97e82e45a53e312cb28e1649f465dd31 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 8 Aug 2025 13:23:21 +0900 Subject: [PATCH 08/13] add tests --- .../common/base/bidi_bridge.rb | 9 +++-- lib/appium_lib_core/common/base/bridge.rb | 3 -- lib/appium_lib_core/common/base/driver.rb | 13 +++---- test/unit/driver_test.rb | 34 +++++++++++++++++++ 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/lib/appium_lib_core/common/base/bidi_bridge.rb b/lib/appium_lib_core/common/base/bidi_bridge.rb index 420d7aea..90259b9a 100644 --- a/lib/appium_lib_core/common/base/bidi_bridge.rb +++ b/lib/appium_lib_core/common/base/bidi_bridge.rb @@ -45,8 +45,13 @@ class BiDiBridge < ::Appium::Core::Base::Bridge # def create_session(capabilities) super - socket_url = @capabilities[:web_socket_url] - @bidi = ::Selenium::WebDriver::BiDi.new(url: socket_url) + + unless @capabilities.nil? + socket_url = @capabilities[:web_socket_url] + @bidi = ::Selenium::WebDriver::BiDi.new(url: socket_url) if socket_url + end + + @capabilities end def get(url) diff --git a/lib/appium_lib_core/common/base/bridge.rb b/lib/appium_lib_core/common/base/bridge.rb index 61b10980..18c04d1d 100644 --- a/lib/appium_lib_core/common/base/bridge.rb +++ b/lib/appium_lib_core/common/base/bridge.rb @@ -116,9 +116,6 @@ def create_session(capabilities) raise ::Selenium::WebDriver::Error::WebDriverError, 'no sessionId in returned payload' unless @session_id @capabilities = json_create(response['capabilities']) - - socket_url = @capabilities[:web_socket_url] - @bidi = ::Selenium::WebDriver::BiDi.new(url: socket_url) if socket_url end # Append +appium:+ prefix for Appium following W3C spec diff --git a/lib/appium_lib_core/common/base/driver.rb b/lib/appium_lib_core/common/base/driver.rb index b48ae3b7..3645e1fe 100644 --- a/lib/appium_lib_core/common/base/driver.rb +++ b/lib/appium_lib_core/common/base/driver.rb @@ -57,6 +57,8 @@ def initialize(bridge: nil, listener: nil, **opts) # rubocop:disable Lint/Missin # internal use @has_bidi = false + ::Selenium::WebDriver::Remote::Bridge.element_class = ::Appium::Core::Element + bridge ||= create_bridge(**opts) add_extensions(bridge.browser) @bridge = listener ? ::Appium::Support::EventFiringBridge.new(bridge, listener, **original_opts) : bridge @@ -80,14 +82,9 @@ def create_bridge(**opts) raise ::Appium::Core::Error::ArgumentError, "Unable to create a driver with parameters: #{opts}" unless opts.empty? - if capabilities['webSocketUrl'] - @has_bidi = true - ::Selenium::WebDriver::Remote::BiDiBridge.element_class = ::Appium::Core::Element - bridge = ::Appium::Core::Base::BiDiBridge.new(**bridge_opts) - else - ::Selenium::WebDriver::Remote::Bridge.element_class = ::Appium::Core::Element - bridge = ::Appium::Core::Base::Bridge.new(**bridge_opts) - end + @has_bidi = capabilities && capabilities['webSocketUrl'] + bridge_clzz = @has_bidi ? ::Appium::Core::Base::BiDiBridge : ::Appium::Core::Base::Bridge + bridge = bridge_clzz.new(**bridge_opts) if session_id.nil? bridge.create_session(capabilities) diff --git a/test/unit/driver_test.rb b/test/unit/driver_test.rb index 70c04c93..7e0aa251 100644 --- a/test/unit/driver_test.rb +++ b/test/unit/driver_test.rb @@ -700,6 +700,40 @@ def test_listener_with_custom_listener_elements assert_equal ::Appium::Core::Element, c_el.first.class end + def test_bidi_bridge + android_mock_create_session_w3c_direct = lambda do |core| + response = { + value: { + sessionId: '1234567890', + capabilities: { + platformName: :android, + automationName: ENV['APPIUM_DRIVER'] || 'uiautomator2', + deviceName: 'Android Emulator', + webSocketUrl: 'ws://192.168.1.49:4723/bidi/fbed26aa-e104-42fc-9f5e-b401dc6cc2bc' + } + } + }.to_json + + stub_request(:post, 'http://127.0.0.1:4723/session') + .to_return(headers: HEADER, status: 200, body: response) + + driver = core.start_driver + + assert_requested(:post, 'http://127.0.0.1:4723/session', times: 1) + driver + end + + capabilities = Caps.android[:capabilities] + capabilities['webSocketUrl'] = true + + core = ::Appium::Core.for capabilities: capabilities + driver = android_mock_create_session_w3c_direct.call(core) + + assert_equal driver.send(:bridge).class, Appium::Core::Base::BiDiBridge + assert_equal driver.instance_variable_get(:@wait_timeout), 30 + assert !driver.send(:bridge).respond_to?(:driver) + end + def test_elements driver = android_mock_create_session From 844232022492ca001caaee57fa0007532bbaf888 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 8 Aug 2025 14:16:29 +0900 Subject: [PATCH 09/13] fix test --- .../common/base/bidi_bridge.rb | 7 ++++++- lib/appium_lib_core/common/base/driver.rb | 1 - lib/appium_lib_core/driver.rb | 4 ++-- test/unit/driver_test.rb | 19 ++++++++++++++++--- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/appium_lib_core/common/base/bidi_bridge.rb b/lib/appium_lib_core/common/base/bidi_bridge.rb index 90259b9a..349805b9 100644 --- a/lib/appium_lib_core/common/base/bidi_bridge.rb +++ b/lib/appium_lib_core/common/base/bidi_bridge.rb @@ -46,9 +46,14 @@ class BiDiBridge < ::Appium::Core::Base::Bridge def create_session(capabilities) super - unless @capabilities.nil? + return @capabilities if @capabilities.nil? + + begin socket_url = @capabilities[:web_socket_url] @bidi = ::Selenium::WebDriver::BiDi.new(url: socket_url) if socket_url + rescue StandardError => e + ::Appium::Logger.warn "WebSocket connection to #{socket_url} for BiDi failed. Error #{e}" + raise end @capabilities diff --git a/lib/appium_lib_core/common/base/driver.rb b/lib/appium_lib_core/common/base/driver.rb index 3645e1fe..8437667c 100644 --- a/lib/appium_lib_core/common/base/driver.rb +++ b/lib/appium_lib_core/common/base/driver.rb @@ -58,7 +58,6 @@ def initialize(bridge: nil, listener: nil, **opts) # rubocop:disable Lint/Missin @has_bidi = false ::Selenium::WebDriver::Remote::Bridge.element_class = ::Appium::Core::Element - bridge ||= create_bridge(**opts) add_extensions(bridge.browser) @bridge = listener ? ::Appium::Support::EventFiringBridge.new(bridge, listener, **original_opts) : bridge diff --git a/lib/appium_lib_core/driver.rb b/lib/appium_lib_core/driver.rb index df531801..634a2a0e 100644 --- a/lib/appium_lib_core/driver.rb +++ b/lib/appium_lib_core/driver.rb @@ -421,8 +421,8 @@ def start_driver(server_url: nil, d_c = DirectConnections.new(@driver.capabilities) @driver.update_sending_request_to(protocol: d_c.protocol, host: d_c.host, port: d_c.port, path: d_c.path) end - rescue Errno::ECONNREFUSED - raise "ERROR: Unable to connect to Appium. Is the server running on #{@custom_url}?" + rescue Errno::ECONNREFUSED => e + raise "ERROR: Unable to connect to Appium. Is the server running on #{@custom_url}? Error: #{e}" end if @http_client.instance_variable_defined? :@additional_headers diff --git a/test/unit/driver_test.rb b/test/unit/driver_test.rb index 7e0aa251..6dd47272 100644 --- a/test/unit/driver_test.rb +++ b/test/unit/driver_test.rb @@ -701,6 +701,10 @@ def test_listener_with_custom_listener_elements end def test_bidi_bridge + # Mock the BiDi WebSocket connection using Minitest + mock_bidi = Minitest::Mock.new + mock_bidi.expect(:close, nil) + android_mock_create_session_w3c_direct = lambda do |core| response = { value: { @@ -709,7 +713,7 @@ def test_bidi_bridge platformName: :android, automationName: ENV['APPIUM_DRIVER'] || 'uiautomator2', deviceName: 'Android Emulator', - webSocketUrl: 'ws://192.168.1.49:4723/bidi/fbed26aa-e104-42fc-9f5e-b401dc6cc2bc' + webSocketUrl: 'ws://127.0.0.1:4723/bidi/fbed26aa-e104-42fc-9f5e-b401dc6cc2bc' } } }.to_json @@ -717,7 +721,10 @@ def test_bidi_bridge stub_request(:post, 'http://127.0.0.1:4723/session') .to_return(headers: HEADER, status: 200, body: response) - driver = core.start_driver + driver = nil + ::Selenium::WebDriver::BiDi.stub(:new, mock_bidi) do + driver = core.start_driver + end assert_requested(:post, 'http://127.0.0.1:4723/session', times: 1) driver @@ -730,8 +737,14 @@ def test_bidi_bridge driver = android_mock_create_session_w3c_direct.call(core) assert_equal driver.send(:bridge).class, Appium::Core::Base::BiDiBridge - assert_equal driver.instance_variable_get(:@wait_timeout), 30 assert !driver.send(:bridge).respond_to?(:driver) + + stub_request(:delete, 'http://127.0.0.1:4723/session/1234567890') + .to_return(headers: HEADER, status: 200, body: { value: nil }.to_json) + + driver.quit + # Verify that close was called exactly once + mock_bidi.verify end def test_elements From 075563b23f5fd739397b46f5c4a9dd93381fbb9e Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 8 Aug 2025 14:44:05 +0900 Subject: [PATCH 10/13] ignore --- test/functional/android/webdriver/bidi_test.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/functional/android/webdriver/bidi_test.rb b/test/functional/android/webdriver/bidi_test.rb index b0cf7b87..c5dfbf14 100644 --- a/test/functional/android/webdriver/bidi_test.rb +++ b/test/functional/android/webdriver/bidi_test.rb @@ -35,8 +35,12 @@ def test_bidi driver.page_source - driver.bidi.remove_callback('log.entryAdded', subscribe_id) - driver.bidi.send_cmd('session.unsubscribe', 'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP']) + begin + driver.bidi.remove_callback('log.entryAdded', subscribe_id) + driver.bidi.send_cmd('session.unsubscribe', 'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP']) + rescue StandardError => e + # ignore + end driver&.quit end From 6d76ffb338e30504a0458c85dab3656d6fe0c367 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 8 Aug 2025 14:54:50 +0900 Subject: [PATCH 11/13] fix rubocop --- test/functional/android/webdriver/bidi_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/android/webdriver/bidi_test.rb b/test/functional/android/webdriver/bidi_test.rb index c5dfbf14..40eb4bc1 100644 --- a/test/functional/android/webdriver/bidi_test.rb +++ b/test/functional/android/webdriver/bidi_test.rb @@ -38,7 +38,7 @@ def test_bidi begin driver.bidi.remove_callback('log.entryAdded', subscribe_id) driver.bidi.send_cmd('session.unsubscribe', 'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP']) - rescue StandardError => e + rescue StandardError # ignore end From 1cf9bf9b5ec5ff1c970bcbab680e8383d7f10818 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 8 Aug 2025 15:40:43 +0900 Subject: [PATCH 12/13] comment out func test for now --- .github/workflows/functional-test.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index df3501b4..621f416e 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -199,10 +199,10 @@ jobs: - target: test/functional/android/driver_test.rb,test/functional/android/patch_test.rb,test/functional/android/android/device_test.rb,test/functional/android/android/search_context_test.rb automation_name: espresso name: test2 - - target: test/functional/android/webdriver/create_session_test.rb,test/functional/android/webdriver/device_test.rb,test/functional/android/webdriver/w3c_actions_test.rb,test/functional/android/webdriver/bidi_test.rb + - target: test/functional/android/webdriver/create_session_test.rb,test/functional/android/webdriver/device_test.rb,test/functional/android/webdriver/w3c_actions_test.rb automation_name: uiautomator2 name: test3 - - target: test/functional/android/webdriver/create_session_test.rb,test/functional/android/webdriver/device_test.rb,test/functional/android/webdriver/w3c_actions_test.rb,test/functional/android/webdriver/bidi_test.rb + - target: test/functional/android/webdriver/create_session_test.rb,test/functional/android/webdriver/device_test.rb,test/functional/android/webdriver/w3c_actions_test.rb automation_name: espresso name: test4 - target: test/functional/android/android/mobile_commands_test.rb @@ -223,6 +223,13 @@ jobs: - target: test/functional/android/android/mjpeg_server_test.rb,test/functional/android/android/image_comparison_test.rb automation_name: espresso name: test10 + # FIXME: rever the comment out after https://github.com/appium/appium/pull/21468 + # - target: test/functional/android/webdriver/bidi_test.rb + # automation_name: uiautomator2 + # name: test11 + # - target: test/functional/android/webdriver/bidi_test.rb + # automation_name: espresso + # name: test12 env: API_LEVEL: 36 From 129547581caa85d5e4aba481d8dcb4924b55b922 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 8 Aug 2025 16:37:09 +0900 Subject: [PATCH 13/13] add rbs --- .../common/base/bidi_bridge.rbs | 25 +++++++++++++++++++ .../appium_lib_core/common/base/bridge.rbs | 2 +- .../common/base/search_context.rbs | 10 +++++++- 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 sig/lib/appium_lib_core/common/base/bidi_bridge.rbs diff --git a/sig/lib/appium_lib_core/common/base/bidi_bridge.rbs b/sig/lib/appium_lib_core/common/base/bidi_bridge.rbs new file mode 100644 index 00000000..0a16a16e --- /dev/null +++ b/sig/lib/appium_lib_core/common/base/bidi_bridge.rbs @@ -0,0 +1,25 @@ +module Appium + module Core + class Base + class BiDiBridge < ::Appium::Core::Base::Bridge + @bidi: ::Selenium::WebDriver::BiDi + + def attach_to: (untyped session_id, untyped platform_name, untyped automation_name) -> untyped + + def create_session: (untyped capabilities) -> ::Appium::Core::Base::Capabilities + + def get: (string url) -> untyped + + def go_back: () -> untyped + + def go_forward: () -> untyped + + def refresh: () -> untyped + + def quit: () -> untyped + + def close: () -> untyped + end + end + end +end diff --git a/sig/lib/appium_lib_core/common/base/bridge.rbs b/sig/lib/appium_lib_core/common/base/bridge.rbs index 35714957..59c86e32 100644 --- a/sig/lib/appium_lib_core/common/base/bridge.rbs +++ b/sig/lib/appium_lib_core/common/base/bridge.rbs @@ -94,7 +94,7 @@ module Appium # core = ::Appium::Core.for(caps) # driver = core.start_driver # - def create_session: (untyped capabilities) -> untyped + def create_session: (untyped capabilities) -> ::Appium::Core::Base::Capabilities # Append +appium:+ prefix for Appium following W3C spec # https://www.w3.org/TR/webdriver/#dfn-validate-capabilities diff --git a/sig/lib/appium_lib_core/common/base/search_context.rbs b/sig/lib/appium_lib_core/common/base/search_context.rbs index 3bcda524..9e72ef78 100644 --- a/sig/lib/appium_lib_core/common/base/search_context.rbs +++ b/sig/lib/appium_lib_core/common/base/search_context.rbs @@ -1 +1,9 @@ -APPIUM_EXTRA_FINDERS: { accessibility_id: "accessibility id", image: "-image", custom: "-custom", uiautomator: "-android uiautomator", viewtag: "-android viewtag", data_matcher: "-android datamatcher", view_matcher: "-android viewmatcher", predicate: "-ios predicate string", class_chain: "-ios class chain" } +module Appium + module Core + class Base + module SearchContext + APPIUM_EXTRA_FINDERS: { Symbol => String } + end + end + end +end