Skip to content

Commit fd23456

Browse files
authored
feat: add BiDi bridge (#631)
* feat: add BiDiBridge * add bidi test * add tests * add bdiege class * add webSocketUrl * add docstring * fix rubocop * add tests * fix test * ignore * fix rubocop * comment out func test for now * add rbs
1 parent f244a36 commit fd23456

File tree

10 files changed

+266
-7
lines changed

10 files changed

+266
-7
lines changed

.github/workflows/functional-test.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,13 @@ jobs:
223223
- target: test/functional/android/android/mjpeg_server_test.rb,test/functional/android/android/image_comparison_test.rb
224224
automation_name: espresso
225225
name: test10
226+
# FIXME: rever the comment out after https://github.com/appium/appium/pull/21468
227+
# - target: test/functional/android/webdriver/bidi_test.rb
228+
# automation_name: uiautomator2
229+
# name: test11
230+
# - target: test/functional/android/webdriver/bidi_test.rb
231+
# automation_name: espresso
232+
# name: test12
226233

227234
env:
228235
API_LEVEL: 36

lib/appium_lib_core/common/base.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@
3030
require_relative 'device/orientation'
3131

3232
# The following files have selenium-webdriver related stuff.
33-
require_relative 'base/driver'
3433
require_relative 'base/bridge'
34+
require_relative 'base/bidi_bridge'
35+
require_relative 'base/driver'
3536
require_relative 'base/capabilities'
3637
require_relative 'base/http_default'
3738
require_relative 'base/search_context'
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# frozen_string_literal: true
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
require_relative 'bridge'
16+
17+
module Appium
18+
module Core
19+
class Base
20+
class BiDiBridge < ::Appium::Core::Base::Bridge
21+
attr_reader :bidi
22+
23+
# Override
24+
# Creates session handling.
25+
#
26+
# @param [::Appium::Core::Base::Capabilities, Hash] capabilities A capability
27+
# @return [::Appium::Core::Base::Capabilities]
28+
#
29+
# @example
30+
#
31+
# opts = {
32+
# caps: {
33+
# platformName: :android,
34+
# automationName: 'uiautomator2',
35+
# platformVersion: '15',
36+
# deviceName: 'Android',
37+
# webSocketUrl: true,
38+
# },
39+
# appium_lib: {
40+
# wait: 30
41+
# }
42+
# }
43+
# core = ::Appium::Core.for(caps)
44+
# driver = core.start_driver
45+
#
46+
def create_session(capabilities)
47+
super
48+
49+
return @capabilities if @capabilities.nil?
50+
51+
begin
52+
socket_url = @capabilities[:web_socket_url]
53+
@bidi = ::Selenium::WebDriver::BiDi.new(url: socket_url) if socket_url
54+
rescue StandardError => e
55+
::Appium::Logger.warn "WebSocket connection to #{socket_url} for BiDi failed. Error #{e}"
56+
raise
57+
end
58+
59+
@capabilities
60+
end
61+
62+
def get(url)
63+
browsing_context.navigate(url)
64+
end
65+
66+
def go_back
67+
browsing_context.traverse_history(-1)
68+
end
69+
70+
def go_forward
71+
browsing_context.traverse_history(1)
72+
end
73+
74+
def refresh
75+
browsing_context.reload
76+
end
77+
78+
def quit
79+
super
80+
ensure
81+
bidi.close
82+
end
83+
84+
def close
85+
execute(:close_window).tap { |handles| bidi.close if handles.empty? }
86+
end
87+
88+
private
89+
90+
def browsing_context
91+
@browsing_context ||= ::Selenium::WebDriver::BiDi::BrowsingContext.new(self)
92+
end
93+
end # class BiDiBridge
94+
end # class Base
95+
end # module Core
96+
end # module Appium

lib/appium_lib_core/common/base/driver.rb

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ def initialize(bridge: nil, listener: nil, **opts) # rubocop:disable Lint/Missin
5454
@devtools = nil
5555
@bidi = nil
5656

57-
# in the selenium webdriver as well
57+
# internal use
58+
@has_bidi = false
59+
5860
::Selenium::WebDriver::Remote::Bridge.element_class = ::Appium::Core::Element
5961
bridge ||= create_bridge(**opts)
6062
add_extensions(bridge.browser)
@@ -79,7 +81,9 @@ def create_bridge(**opts)
7981

8082
raise ::Appium::Core::Error::ArgumentError, "Unable to create a driver with parameters: #{opts}" unless opts.empty?
8183

82-
bridge = ::Appium::Core::Base::Bridge.new(**bridge_opts)
84+
@has_bidi = capabilities && capabilities['webSocketUrl']
85+
bridge_clzz = @has_bidi ? ::Appium::Core::Base::BiDiBridge : ::Appium::Core::Base::Bridge
86+
bridge = bridge_clzz.new(**bridge_opts)
8387

8488
if session_id.nil?
8589
bridge.create_session(capabilities)
@@ -996,6 +1000,28 @@ def execute_driver(script: '', type: 'webdriverio', timeout_ms: nil)
9961000
def convert_to_element(response_id)
9971001
@bridge.convert_to_element response_id
9981002
end
1003+
1004+
# Return bidi instance
1005+
# @return [::Selenium::WebDriver::BiDi]
1006+
#
1007+
# @example
1008+
#
1009+
# log_entries = []
1010+
# driver.bidi.send_cmd('session.subscribe', 'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP'])
1011+
# subscribe_id = driver.bidi.add_callback('log.entryAdded') do |params|
1012+
# log_entries << params
1013+
# end
1014+
# driver.page_source
1015+
#
1016+
# driver.bidi.remove_callback('log.entryAdded', subscribe_id)
1017+
# driver.bidi.send_cmd('session.unsubscribe', 'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP'])
1018+
#
1019+
def bidi
1020+
return @bridge.bidi if @has_bidi
1021+
1022+
msg = 'BiDi must be enabled by providing webSocketUrl capability to true'
1023+
raise(::Selenium::WebDriver::Error::WebDriverError, msg)
1024+
end
9991025
end # class Driver
10001026
end # class Base
10011027
end # module Core

lib/appium_lib_core/driver.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,8 +421,8 @@ def start_driver(server_url: nil,
421421
d_c = DirectConnections.new(@driver.capabilities)
422422
@driver.update_sending_request_to(protocol: d_c.protocol, host: d_c.host, port: d_c.port, path: d_c.path)
423423
end
424-
rescue Errno::ECONNREFUSED
425-
raise "ERROR: Unable to connect to Appium. Is the server running on #{@custom_url}?"
424+
rescue Errno::ECONNREFUSED => e
425+
raise "ERROR: Unable to connect to Appium. Is the server running on #{@custom_url}? Error: #{e}"
426426
end
427427

428428
if @http_client.instance_variable_defined? :@additional_headers
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module Appium
2+
module Core
3+
class Base
4+
class BiDiBridge < ::Appium::Core::Base::Bridge
5+
@bidi: ::Selenium::WebDriver::BiDi
6+
7+
def attach_to: (untyped session_id, untyped platform_name, untyped automation_name) -> untyped
8+
9+
def create_session: (untyped capabilities) -> ::Appium::Core::Base::Capabilities
10+
11+
def get: (string url) -> untyped
12+
13+
def go_back: () -> untyped
14+
15+
def go_forward: () -> untyped
16+
17+
def refresh: () -> untyped
18+
19+
def quit: () -> untyped
20+
21+
def close: () -> untyped
22+
end
23+
end
24+
end
25+
end

sig/lib/appium_lib_core/common/base/bridge.rbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ module Appium
9494
# core = ::Appium::Core.for(caps)
9595
# driver = core.start_driver
9696
#
97-
def create_session: (untyped capabilities) -> untyped
97+
def create_session: (untyped capabilities) -> ::Appium::Core::Base::Capabilities
9898

9999
# Append +appium:+ prefix for Appium following W3C spec
100100
# https://www.w3.org/TR/webdriver/#dfn-validate-capabilities
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
1-
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" }
1+
module Appium
2+
module Core
3+
class Base
4+
module SearchContext
5+
APPIUM_EXTRA_FINDERS: { Symbol => String }
6+
end
7+
end
8+
end
9+
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
require 'test_helper'
16+
17+
# $ rake test:func:android TEST=test/functional/android/webdriver/bidi_test.rb
18+
class AppiumLibCoreTest
19+
module WebDriver
20+
class BidiTest < AppiumLibCoreTest::Function::TestCase
21+
def test_bidi
22+
caps = Caps.android
23+
caps[:capabilities]['webSocketUrl'] = true
24+
core = ::Appium::Core.for(caps)
25+
26+
driver = core.start_driver
27+
assert !driver.capabilities.nil?
28+
29+
log_entries = []
30+
31+
driver.bidi.send_cmd('session.subscribe', 'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP'])
32+
subscribe_id = driver.bidi.add_callback('log.entryAdded') do |params|
33+
log_entries << params
34+
end
35+
36+
driver.page_source
37+
38+
begin
39+
driver.bidi.remove_callback('log.entryAdded', subscribe_id)
40+
driver.bidi.send_cmd('session.unsubscribe', 'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP'])
41+
rescue StandardError
42+
# ignore
43+
end
44+
45+
driver&.quit
46+
end
47+
end
48+
end
49+
end

test/unit/driver_test.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,53 @@ def test_listener_with_custom_listener_elements
700700
assert_equal ::Appium::Core::Element, c_el.first.class
701701
end
702702

703+
def test_bidi_bridge
704+
# Mock the BiDi WebSocket connection using Minitest
705+
mock_bidi = Minitest::Mock.new
706+
mock_bidi.expect(:close, nil)
707+
708+
android_mock_create_session_w3c_direct = lambda do |core|
709+
response = {
710+
value: {
711+
sessionId: '1234567890',
712+
capabilities: {
713+
platformName: :android,
714+
automationName: ENV['APPIUM_DRIVER'] || 'uiautomator2',
715+
deviceName: 'Android Emulator',
716+
webSocketUrl: 'ws://127.0.0.1:4723/bidi/fbed26aa-e104-42fc-9f5e-b401dc6cc2bc'
717+
}
718+
}
719+
}.to_json
720+
721+
stub_request(:post, 'http://127.0.0.1:4723/session')
722+
.to_return(headers: HEADER, status: 200, body: response)
723+
724+
driver = nil
725+
::Selenium::WebDriver::BiDi.stub(:new, mock_bidi) do
726+
driver = core.start_driver
727+
end
728+
729+
assert_requested(:post, 'http://127.0.0.1:4723/session', times: 1)
730+
driver
731+
end
732+
733+
capabilities = Caps.android[:capabilities]
734+
capabilities['webSocketUrl'] = true
735+
736+
core = ::Appium::Core.for capabilities: capabilities
737+
driver = android_mock_create_session_w3c_direct.call(core)
738+
739+
assert_equal driver.send(:bridge).class, Appium::Core::Base::BiDiBridge
740+
assert !driver.send(:bridge).respond_to?(:driver)
741+
742+
stub_request(:delete, 'http://127.0.0.1:4723/session/1234567890')
743+
.to_return(headers: HEADER, status: 200, body: { value: nil }.to_json)
744+
745+
driver.quit
746+
# Verify that close was called exactly once
747+
mock_bidi.verify
748+
end
749+
703750
def test_elements
704751
driver = android_mock_create_session
705752

0 commit comments

Comments
 (0)