Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement search context and UA based on selenium 4.21 #480

Merged
merged 31 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
268f40e
set the deps destination [skip ci]
KazuCocoa Jun 17, 2023
8fc3ce4
Merge branch 'master' into selenium-12141
KazuCocoa Jun 17, 2023
5ac21dd
use github
KazuCocoa Jun 17, 2023
b4533ca
Merge branch 'master' into selenium-12141
KazuCocoa Aug 13, 2023
3499f13
Merge branch 'master' into selenium-12141
KazuCocoa Aug 24, 2023
96cfa03
Merge branch 'master' into selenium-12141
KazuCocoa Sep 4, 2023
0938833
Merge branch 'master' into selenium-12141
KazuCocoa Nov 4, 2023
d69922a
Merge branch 'master' into selenium-12141
KazuCocoa Feb 3, 2024
04677c9
mofidy to use extra finders
KazuCocoa Feb 3, 2024
cf178cf
fix lint
KazuCocoa Feb 3, 2024
b86906c
add ua
KazuCocoa Feb 3, 2024
1c4fa9b
revert unnecessary naming changes
KazuCocoa Feb 3, 2024
1927c6f
add new line
KazuCocoa Feb 3, 2024
5e4aeb6
revert docstring
KazuCocoa Feb 3, 2024
4b85ae0
simplify a bit
KazuCocoa Feb 3, 2024
34afcb9
remove request
KazuCocoa Feb 3, 2024
956fe2c
add example of add_command
KazuCocoa Feb 3, 2024
4392936
chore: update to the latest imple
KazuCocoa Feb 4, 2024
f607d4f
set element with ::Selenium::WebDriver::Remote::Bridge.element_class …
KazuCocoa Feb 4, 2024
5ba3244
fix lint
KazuCocoa Feb 4, 2024
7dfec92
removed unnecessary override
KazuCocoa Feb 4, 2024
0d31ff8
Merge branch 'master' into selenium-12141
KazuCocoa Feb 5, 2024
2a6abdf
Merge branch 'master' into selenium-12141
KazuCocoa May 15, 2024
2f38784
Update Gemfile
KazuCocoa May 15, 2024
adc279d
add bundle exec
KazuCocoa May 15, 2024
ef3e469
Merge branch 'master' into selenium-12141
KazuCocoa May 15, 2024
9749e20
remove completed todo
KazuCocoa May 15, 2024
c3daf5c
Merge branch 'master' into selenium-12141
KazuCocoa May 16, 2024
5dc8c76
use selenium webdriver 4.21
KazuCocoa May 16, 2024
ae74d1f
Merge branch 'master' into selenium-12141
KazuCocoa May 16, 2024
b319a09
update changelog
KazuCocoa May 17, 2024
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
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
source 'https://rubygems.org'

gem 'selenium-webdriver', github: 'SeleniumHQ/selenium', branch: 'rb-appium'

# Specify your gem's dependencies in appium_lib_core.gemspec
gemspec
8 changes: 8 additions & 0 deletions lib/appium_lib_core/common/base/bridge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
module Appium
module Core
class Base
class LocatorConverter
def convert(how, what)
[how, what]
end
end # LocatorConverter

class Bridge < ::Selenium::WebDriver::Remote::Bridge
include Device::DeviceLock
include Device::Keyboard
Expand All @@ -33,6 +39,8 @@ class Bridge < ::Selenium::WebDriver::Remote::Bridge
include Device::ExecuteDriver
include Device::Orientation

::Selenium::WebDriver::Remote::Bridge.locator_converter = LocatorConverter.new

# Prefix for extra capability defined by W3C
APPIUM_PREFIX = 'appium:'

Expand Down
3 changes: 2 additions & 1 deletion lib/appium_lib_core/common/base/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ class Driver < ::Selenium::WebDriver::Driver
include ::Selenium::WebDriver::DriverExtensions::HasWebStorage

include ::Appium::Core::Base::Rotatable
include ::Appium::Core::Base::SearchContext
include ::Appium::Core::Base::TakesScreenshot
include ::Appium::Core::Base::HasRemoteStatus
include ::Appium::Core::Base::HasLocation
include ::Appium::Core::Base::HasNetworkConnection

include ::Appium::Core::Waitable

::Selenium::WebDriver::SearchContext.extra_finders = EXTRA_FINDERS

# Private API.
# Do not use this for general use. Used by flutter driver to get bridge for creating a new element
attr_reader :bridge
Expand Down
15 changes: 13 additions & 2 deletions lib/appium_lib_core/common/base/http_default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ module RequestHeaders
class Default < ::Selenium::WebDriver::Remote::Http::Default
attr_reader :additional_headers

::Selenium::WebDriver::Remote::Http::Common.user_agent = \
"appium/ruby_lib_core/#{VERSION} (#{::Selenium::WebDriver::Remote::Http::Common.user_agent})"

# override
def initialize(open_timeout: nil, read_timeout: nil)
@open_timeout = open_timeout
Expand All @@ -39,6 +42,16 @@ def initialize(open_timeout: nil, read_timeout: nil)
super
end

def set_additional_header(key, value)
@additional_headers[key] = value
# ::Selenium::WebDriver::Remote::Http::Common.extra_headers = @additional_headers
end

def delete_additional_header(key)
@additional_headers.delete key
# ::Selenium::WebDriver::Remote::Http::Common.extra_headers = @additional_headers
end

# Update <code>server_url</code> provided when ruby_lib _core created a default http client.
# Set <code>@http</code> as nil to re-create http client for the <code>server_url</code>
#
Expand All @@ -60,9 +73,7 @@ def update_sending_request_to(scheme:, host:, port:, path:)
end

def request(verb, url, headers, payload, redirects = 0)
headers['User-Agent'] = "appium/ruby_lib_core/#{VERSION} (#{headers['User-Agent']})"
headers = headers.merge @additional_headers unless @additional_headers.empty?

super(verb, url, headers, payload, redirects)
end

Expand Down
200 changes: 13 additions & 187 deletions lib/appium_lib_core/common/base/search_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,190 +12,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.

module Appium
module Core
class Base
module SearchContext
# referenced: ::Selenium::WebDriver::SearchContext

FINDERS = ::Selenium::WebDriver::SearchContext::FINDERS.merge(
accessibility_id: 'accessibility id',
image: '-image',
custom: '-custom',
# Android
uiautomator: '-android uiautomator', # Unavailable in Espresso
viewtag: '-android viewtag', # Available in Espresso
data_matcher: '-android datamatcher', # Available in Espresso
view_matcher: '-android viewmatcher', # Available in Espresso
# iOS
predicate: '-ios predicate string',
class_chain: '-ios class chain',
# Windows with windows prefix
# @deprecated
windows_uiautomation: '-windows uiautomation',
# Tizen with Tizen prefix
tizen_uiautomation: '-tizen uiautomation'
)

# rubocop:disable Layout/LineLength
#
# Find the first element matching the given arguments
#
# - Android can find with uiautomator like a {http://developer.android.com/tools/help/uiautomator/UiSelector.html UISelector}.
# - iOS can find with a {https://developer.apple.com/library/ios/documentation/ToolsLanguages/Reference/UIAWindowClassReference/UIAWindow/UIAWindow.html#//apple_ref/doc/uid/TP40009930 UIAutomation command}.
# - iOS, only for XCUITest(WebDriverAgent), can find with a {https://github.com/facebook/WebDriverAgent/wiki/Queries class chain}
#
# == Find with image
# Return an element if current view has a partial image. The logic depends on template matching by OpenCV.
# {https://github.com/appium/appium/blob/1.x/docs/en/writing-running-appium/image-comparison.md image-comparison}
#
# You can handle settings for the comparision following {https://github.com/appium/appium-base-driver/blob/master/lib/basedriver/device-settings.js#L6 here}
#
# == Espresso viewmatcher and datamatcher
# Espresso has {https://developer.android.com/training/testing/espresso/basics _onView_ matcher}
# and {https://medium.com/androiddevelopers/adapterviews-and-espresso-f4172aa853cf _onData_ matcher} for more reference
# that allows you to target adapters instead of Views. This method find methods based on reflections
#
# This is a selector strategy that allows users to pass a selector of the form:
#
# <code>{ name: '<name>', args: ['arg1', 'arg2', '...'], class: '<optional class>' }</code>
#
# - _name_: The name of a method to invoke. The method must return
# a Hamcrest {http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html Matcher}
# - _args_: The args provided to the method
# - _class_: The class name that the method is part of (defaults to <code>org.hamcrest.Matchers</code>).
# Can be fully qualified, or simple, and simple defaults to <code>androidx.test.espresso.matcher</code> package
# (e.g.: <code>class=CursorMatchers</code> fully qualified is <code>class=androidx.test.espresso.matcher.CursorMatchers</code>
#
# See example how to send viewmatcher and datamatcher in Ruby client
#
#
# @overload find_element(how, what)
# @param [Symbol, String] how The method to find the element by
# @param [String] what The locator to use
#
# @overload find_element(opts)
# @param [Hash] opts Find options
# @option opts [Symbol] :how Key named after the method to find the element by, containing the locator
# @return [Element]
# @raise [Error::NoSuchElementError] if the element doesn't exist
#
# @example Find element with each keys
#
# # with accessibility id. All platforms.
# @driver.find_elements :accessibility_id, 'Animation'
# @driver.find_elements :accessibility_id, 'Animation'
#
# # with base64 encoded template image. All platforms.
# @driver.find_elements :image, Base64.strict_encode64(File.read(file_path))
#
# # For Android
# ## With uiautomator
# @driver.find_elements :uiautomator, 'new UiSelector().clickable(true)'
# ## With viewtag, but only for Espresso
# ## 'setTag'/'getTag' in https://developer.android.com/reference/android/view/View
# @driver.find_elements :viewtag, 'new UiSelector().clickable(true)'
# # With data_matcher. The argument should be JSON format.
# @driver.find_elements :data_matcher, { name: 'hasEntry', args: %w(title Animation) }.to_json
#
# # For iOS
# ## With :predicate
# @driver.find_elements :predicate, "isWDVisible == 1"
# @driver.find_elements :predicate, 'wdName == "Buttons"'
# @driver.find_elements :predicate, 'wdValue == "SearchBar" AND isWDDivisible == 1'
#
# ## With Class Chain
# ### select the third child button of the first child window element
# @driver.find_elements :class_chain, 'XCUIElementTypeWindow/XCUIElementTypeButton[3]'
# ### select all the children windows
# @driver.find_elements :class_chain, 'XCUIElementTypeWindow'
# ### select the second last child of the second child window
# @driver.find_elements :class_chain, 'XCUIElementTypeWindow[2]/XCUIElementTypeAny[-2]'
# ### matching predicate. <code>'</code> is the mark.
# @driver.find_elements :class_chain, 'XCUIElementTypeWindow['visible = 1]['name = "bla"']'
# ### containing predicate. '$' is the mark.
# ### Require appium-xcuitest-driver 2.54.0+. PR: https://github.com/facebook/WebDriverAgent/pull/707/files
# @driver.find_elements :class_chain, 'XCUIElementTypeWindow[$name = \"bla$$$bla\"$]'
# e = find_element :class_chain, "**/XCUIElementTypeWindow[$name == 'Buttons'$]"
# e.tag_name #=> "XCUIElementTypeWindow"
# e = find_element :class_chain, "**/XCUIElementTypeStaticText[$name == 'Buttons'$]"
# e.tag_name #=> "XCUIElementTypeStaticText"
#
# # For Windows
# # @deprecated
# @driver.find_elements :windows_uiautomation, '....'
#
# # For Tizen
# @driver.find_elements :tizen_uiautomation, '....'
#
# rubocop:enable Layout/LineLength
def find_element(*args)
how, what = extract_args(args)
by = _set_by_from_finders(how)
begin
bridge.find_element_by by, what.to_s, ref
rescue Selenium::WebDriver::Error::TimeoutError
raise Selenium::WebDriver::Error::NoSuchElementError
end
end

#
# Find all elements matching the given arguments
#
# @return [Array<Selenium::WebDriver::Element>]
#
# @see SearchContext#find_elements
#
def find_elements(*args)
how, what = extract_args(args)
by = _set_by_from_finders(how)
begin
bridge.find_elements_by by, what.to_s, ref
rescue Selenium::WebDriver::Error::TimeoutError
[]
end
end

private

def _set_by_from_finders(how)
if how == :windows_uiautomation
::Appium::Logger.warn(
'[DEPRECATION] :windows_uiautomation is deprecated. Please use other locators.'
)
end

by = FINDERS[how.to_sym]
unless by
raise ::Appium::Core::Error::ArgumentError,
"cannot find element by #{how.inspect}. Available finders are #{FINDERS.keys}."
end

by
end

def extract_args(args)
case args.size
when 2
args
when 1
arg = args.first

unless arg.respond_to?(:shift)
raise ::Appium::Core::Error::ArgumentError, "expected #{arg.inspect}:#{arg.class} to respond to #shift"
end

# this will be a single-entry hash, so use #shift over #first or #[]
arr = arg.dup.shift

raise ::Appium::Core::Error::ArgumentError, "expected #{arr.inspect} to have 2 elements" unless arr.size == 2

arr
else
raise ::Appium::Core::Error::ArgumentError, "wrong number of arguments (#{args.size} for 2)"
end
end
end # module SearchContext
end # class Base
end # module Core
end # module Appium
EXTRA_FINDERS = {
accessibility_id: 'accessibility id',
image: '-image',
custom: '-custom',
# Android
uiautomator: '-android uiautomator', # Unavailable in Espresso
viewtag: '-android viewtag', # Available in Espresso
data_matcher: '-android datamatcher', # Available in Espresso
view_matcher: '-android viewmatcher', # Available in Espresso
# iOS
predicate: '-ios predicate string',
class_chain: '-ios class chain'
}.freeze
4 changes: 2 additions & 2 deletions lib/appium_lib_core/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ def start_driver(server_url: nil,

if @enable_idempotency_header
if @http_client.instance_variable_defined? :@additional_headers
@http_client.additional_headers[Appium::Core::Base::Http::RequestHeaders::KEYS[:idempotency]] = SecureRandom.uuid
@http_client.set_additional_header Appium::Core::Base::Http::RequestHeaders::KEYS[:idempotency], SecureRandom.uuid
else
::Appium::Logger.warn 'No additional_headers attribute in this http client instance'
end
Expand All @@ -432,7 +432,7 @@ def start_driver(server_url: nil,

if @http_client.instance_variable_defined? :@additional_headers
# We only need the key for a new session request. Should remove it for other following commands.
@http_client.additional_headers.delete Appium::Core::Base::Http::RequestHeaders::KEYS[:idempotency]
@http_client.delete_additional_header Appium::Core::Base::Http::RequestHeaders::KEYS[:idempotency]
end

# TODO: this method can be removed after releasing Appium 2.0, and after a while
Expand Down
3 changes: 2 additions & 1 deletion lib/appium_lib_core/element.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ module Core
# Implement useful features for element.
# Patch for Selenium Webdriver.
class Element < ::Selenium::WebDriver::Element
include ::Appium::Core::Base::SearchContext
include ::Appium::Core::Base::TakesScreenshot

::Selenium::WebDriver::SearchContext.extra_finders = EXTRA_FINDERS

# Retuns the element id.
#
# @return [String]
Expand Down
48 changes: 25 additions & 23 deletions test/unit/driver_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -442,29 +442,31 @@ def _android_mock_create_session_w3c_with_custom_http_client(core)

# https://www.w3.org/TR/webdriver1/
def test_search_context_in_element_class
assert_equal 22, ::Appium::Core::Element::FINDERS.length
assert_equal({ class: 'class name',
class_name: 'class name',
css: 'css selector', # Defined in W3C spec
id: 'id',
link: 'link text', # Defined in W3C spec
link_text: 'link text', # Defined in W3C spec
name: 'name',
partial_link_text: 'partial link text', # Defined in W3C spec
relative: 'relative', # Defined in Selenium
tag_name: 'tag name', # Defined in W3C spec
xpath: 'xpath', # Defined in W3C spec
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',
windows_uiautomation: '-windows uiautomation',
tizen_uiautomation: '-tizen uiautomation' }, ::Appium::Core::Element::FINDERS)
assert_equal(
{
class: 'class name',
class_name: 'class name',
css: 'css selector', # Defined in W3C spec
id: 'id',
link: 'link text', # Defined in W3C spec
link_text: 'link text', # Defined in W3C spec
name: 'name',
partial_link_text: 'partial link text', # Defined in W3C spec
relative: 'relative', # Defined in Selenium
tag_name: 'tag name', # Defined in W3C spec
xpath: 'xpath', # Defined in W3C spec
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'
},
::Appium::Core::Element::FINDERS.merge(::Selenium::WebDriver::SearchContext.extra_finders)
)
end

def test_attach_to_an_existing_session
Expand Down
Loading