Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/functional-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lib/appium_lib_core/common/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
96 changes: 96 additions & 0 deletions lib/appium_lib_core/common/base/bidi_bridge.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# 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: :android,
# automationName: 'uiautomator2',
# platformVersion: '15',
# deviceName: 'Android',
# webSocketUrl: true,
# },
# appium_lib: {
# wait: 30
# }
# }
# core = ::Appium::Core.for(caps)
# driver = core.start_driver
#
def create_session(capabilities)
super

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
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
30 changes: 28 additions & 2 deletions lib/appium_lib_core/common/base/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ def initialize(bridge: nil, listener: nil, **opts) # rubocop:disable Lint/Missin
@devtools = nil
@bidi = nil

# in the selenium webdriver as well
# internal use
@has_bidi = false

::Selenium::WebDriver::Remote::Bridge.element_class = ::Appium::Core::Element
bridge ||= create_bridge(**opts)
add_extensions(bridge.browser)
Expand All @@ -79,7 +81,9 @@ 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)
@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)
Expand Down Expand Up @@ -996,6 +1000,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
Expand Down
4 changes: 2 additions & 2 deletions lib/appium_lib_core/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions sig/lib/appium_lib_core/common/base/bidi_bridge.rbs
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion sig/lib/appium_lib_core/common/base/bridge.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion sig/lib/appium_lib_core/common/base/search_context.rbs
Original file line number Diff line number Diff line change
@@ -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
49 changes: 49 additions & 0 deletions test/functional/android/webdriver/bidi_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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/bidi_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

begin
driver.bidi.remove_callback('log.entryAdded', subscribe_id)
driver.bidi.send_cmd('session.unsubscribe', 'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP'])
rescue StandardError
# ignore
end

driver&.quit
end
end
end
end
47 changes: 47 additions & 0 deletions test/unit/driver_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,53 @@ def test_listener_with_custom_listener_elements
assert_equal ::Appium::Core::Element, c_el.first.class
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: {
sessionId: '1234567890',
capabilities: {
platformName: :android,
automationName: ENV['APPIUM_DRIVER'] || 'uiautomator2',
deviceName: 'Android Emulator',
webSocketUrl: 'ws://127.0.0.1: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 = 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
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 !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
driver = android_mock_create_session

Expand Down
Loading