Skip to content
Closed
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
51 changes: 50 additions & 1 deletion lib/appium_lib_core/common/base/bridge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,62 @@ def element_attribute(element, name)
end

# For Appium
def active_element
convert_to_element(execute(:get_active_element))
end

alias switch_to_active_element active_element

# For Appium
def find_element_by(how, what, parent_ref = [])
how, what = @locator_converter.convert(how, what)

# steep:ignore:start
return execute_atom(:findElements, ::Selenium::WebDriver::Support::RelativeLocator.new(what).as_json).first if how == 'relative'

parent_type, parent_id = parent_ref
id = case parent_type
when :element
execute :find_child_element, { id: parent_id }, { using: how, value: what.to_s }
when :shadow_root
execute :find_shadow_child_element, { id: parent_id }, { using: how, value: what.to_s }
else
execute :find_element, {}, { using: how, value: what.to_s }
end
# steep:ignore:end

convert_to_element(id)
end

# For Appium
def find_elements_by(how, what, parent_ref = [])
how, what = @locator_converter.convert(how, what)

# steep:ignore:start
return execute_atom :findElements, ::Selenium::WebDriver::Support::RelativeLocator.new(what).as_json if how == 'relative'

parent_type, parent_id = parent_ref
ids = case parent_type
when :element
execute :find_child_elements, { id: parent_id }, { using: how, value: what.to_s }
when :shadow_root
execute :find_shadow_child_elements, { id: parent_id }, { using: how, value: what.to_s }
else
execute :find_elements, {}, { using: how, value: what.to_s }
end

ids.map { |id| convert_to_element(id) }
# steep:ignore:end
end

# For Appium
# @param [Hash] id The id which can get as a response from server
# @return [::Appium::Core::Element]
def convert_to_element(id)
::Appium::Core::Element.new self, element_id_from(id)
return id unless id.is_a?(Hash)

cached_attributes = id.except('ELEMENT', ::Appium::Core::Element::ELEMENT_KEY)
::Appium::Core::Element.new self, element_id_from(id), cached_attributes
end

# For Appium
Expand Down
77 changes: 77 additions & 0 deletions lib/appium_lib_core/element.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,70 @@ module Core
# Implement useful features for element.
# Patch for Selenium Webdriver.
class Element < ::Selenium::WebDriver::Element
# Holds attributes returned with the element response when
# +shouldUseCompactResponses+ is +false+ and +elementResponseAttributes+ is set.
class Cache
# @return [Hash]
attr_reader :attributes

def initialize(attributes = {})
@attributes = attributes || {}
end

def attribute(name)
attr_key = "attribute/#{name}"
return @attributes[attr_key] if @attributes.key?(attr_key)

@attributes[name.to_s]
end

alias [] attribute

def rect
data = @attributes['rect']
return nil unless data.is_a?(Hash)

::Selenium::WebDriver::Rectangle.new(data['x'], data['y'], data['width'], data['height'])
end

def location
r = rect
return nil if r.nil?

::Selenium::WebDriver::Point.new(r.x, r.y)
end

def size
r = rect
return nil if r.nil?

::Selenium::WebDriver::Dimension.new(r.width, r.height)
end

def tag_name
@attributes['name'] || @attributes['tag_name']
end

def method_missing(method_name, *args, &block)
return if [:to_hash, :to_ary].include?(method_name)

key = method_name.to_s.delete_suffix('?')
hyphenated_key = key.tr('_', '-')

[key, hyphenated_key, "attribute/#{key}", "attribute/#{hyphenated_key}"].each do |candidate|
return @attributes[candidate] if @attributes.key?(candidate)
end

return nil if respond_to?(method_name)

super
end

def respond_to_missing?(_method_name, _include_private = false)
true
end
end

include ::Appium::Core::Base::TakesScreenshot

# steep:ignore:start
Expand All @@ -32,6 +96,19 @@ class Element < ::Selenium::WebDriver::Element
#
attr_reader :id

def initialize(bridge, id, cached_attributes = {})
super(bridge, id) # steep:ignore
@cached_attributes = cached_attributes || {}
end

# Returns cached attributes returned with the element response when
# +shouldUseCompactResponses+ is +false+ and +elementResponseAttributes+ is set.
#
# @return [Appium::Core::Element::Cache]
def cached
@cached ||= Cache.new(@cached_attributes)
end

# Returns the value of attributes like below. Read each platform to know more details.
#
# uiautomator2: https://github.com/appium/appium-uiautomator2-server/blob/203cc7e57ce477f3cff5d95b135d1b3450a6033a/app/src/main/java/io/appium/uiautomator2/utils/Attribute.java#L19
Expand Down
4 changes: 2 additions & 2 deletions lib/appium_lib_core/support/event_firing_bridge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ def find_element_by(how, what, parent = nil)
@delegate.find_element_by how, what, parent
end

::Appium::Core::Element.new self, e.ref.last
::Appium::Core::Element.new self, e.ref.last, e.cached&.attributes
end

def find_elements_by(how, what, parent = nil)
es = dispatch(:find, how, what, driver) do
@delegate.find_elements_by(how, what, parent)
end

es.map { |e| ::Appium::Core::Element.new self, e.ref.last }
es.map { |e| ::Appium::Core::Element.new self, e.ref.last, e.cached&.attributes }
end

private
Expand Down
8 changes: 8 additions & 0 deletions sig/lib/appium_lib_core/common/base/bridge.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,16 @@ module Appium
def element_attribute: (untyped element, untyped name) -> untyped

# For Appium
def active_element: () -> untyped

alias switch_to_active_element active_element

# For Appium
def find_element_by: (untyped how, untyped what, ?untyped parent_ref) -> untyped

# For Appium
def find_elements_by: (untyped how, untyped what, ?untyped parent_ref) -> untyped

# For Appium
# @param [Hash] id The id which can get as a response from server
# @return [::Appium::Core::Element]
Expand Down
32 changes: 31 additions & 1 deletion sig/lib/appium_lib_core/element.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,42 @@ module Appium
module Core
# Element class inheriting from Selenium::WebDriver::Element
class Element < ::Selenium::WebDriver::Element
class Cache
@attributes: Hash[untyped, untyped]

attr_reader attributes: Hash[untyped, untyped]

def initialize: (?Hash[untyped, untyped] attributes) -> void

def attribute: (untyped name) -> untyped

alias [] attribute

def rect: () -> untyped

def location: () -> untyped

def size: () -> untyped

def tag_name: () -> untyped

def method_missing: (untyped method_name, *untyped args) { (?) -> untyped } -> (nil | untyped)

def respond_to_missing?: (untyped _method_name, ?bool _include_private) -> true
end

include ::Appium::Core::Base::TakesScreenshot

def initialize: (untyped bridge, untyped id) -> void
@cached_attributes: Hash[untyped, untyped]

@cached: Cache?

def initialize: (untyped bridge, untyped id, ?Hash[untyped, untyped] cached_attributes) -> void

attr_reader id: Array[untyped]

def cached: () -> Cache

def method_missing: (untyped method_name, *untyped args) { (?) -> untyped } -> (nil | untyped)

def respond_to_missing?: (untyped _method_name, bool _include_private) -> true
Expand Down
68 changes: 68 additions & 0 deletions test/unit/common/element_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,73 @@ def test_element_screenshot
assert_equal 'iVBORw0KGgoAAAANSUhEUgAABDgAAAB+CAIAAABOPDa6AAAAAX', e.screenshot
assert_requested(:get, "#{SESSION}/element/id/screenshot", times: 1)
end

def test_element_cached_attributes
stub_request(:post, "#{SESSION}/element")
.with(body: { using: 'id', value: 'example' }.to_json)
.to_return(headers: HEADER, status: 200, body: {
value: {
::Appium::Core::Element::ELEMENT_KEY => 'element_id_1',
'ELEMENT' => 'element_id_1',
'name' => 'android.widget.TextView',
'text' => 'Hello World',
'enabled' => true,
'displayed' => false,
'selected' => false,
'attribute/content-desc' => 'greeting_label',
'rect' => { 'x' => 10, 'y' => 20, 'width' => 100, 'height' => 50 }
},
sessionId: SESSION,
status: 0
}.to_json)

e = @driver.find_element id: 'example'
assert_equal 'element_id_1', e.id
assert_equal 'android.widget.TextView', e.cached.name
assert_equal 'android.widget.TextView', e.cached.tag_name
assert_equal 'Hello World', e.cached.text
assert_equal true, e.cached.enabled
assert_equal true, e.cached.enabled?
assert_equal false, e.cached.displayed
assert_equal false, e.cached.displayed?
assert_equal false, e.cached.selected?
assert_equal 'greeting_label', e.cached.content_desc
assert_equal 'greeting_label', e.cached.attribute('content-desc')
assert_equal 'greeting_label', e.cached['content-desc']
assert_equal ::Selenium::WebDriver::Rectangle.new(10, 20, 100, 50), e.cached.rect
assert_equal ::Selenium::WebDriver::Point.new(10, 20), e.cached.location
assert_equal ::Selenium::WebDriver::Dimension.new(100, 50), e.cached.size
assert_nil e.cached.non_existent_attribute
assert_nil e.cached.attributes[::Appium::Core::Element::ELEMENT_KEY]
assert_nil e.cached.attributes['ELEMENT']
end

def test_elements_cached_attributes
stub_request(:post, "#{SESSION}/elements")
.with(body: { using: 'id', value: 'example' }.to_json)
.to_return(headers: HEADER, status: 200, body: {
value: [
{
::Appium::Core::Element::ELEMENT_KEY => 'element_id_1',
'text' => 'First',
'attribute/resource-id' => 'com.example:id/first'
},
{
::Appium::Core::Element::ELEMENT_KEY => 'element_id_2',
'text' => 'Second',
'attribute/resource-id' => 'com.example:id/second'
}
],
sessionId: SESSION,
status: 0
}.to_json)

els = @driver.find_elements id: 'example'
assert_equal 2, els.length
assert_equal 'First', els[0].cached.text
assert_equal 'com.example:id/first', els[0].cached.resource_id
assert_equal 'Second', els[1].cached.text
assert_equal 'com.example:id/second', els[1].cached.resource_id
end
end
end
7 changes: 6 additions & 1 deletion test/unit/driver_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -888,10 +888,15 @@ def test_elements

def test_convert_element
driver = android_mock_create_session
response = { 'element-6066-11e4-a52e-4f735466cecf' => 'test-element-id', 'ELEMENT' => 'test-element-id' }
response = {
'element-6066-11e4-a52e-4f735466cecf' => 'test-element-id',
'ELEMENT' => 'test-element-id',
'text' => 'sample text'
}
e = driver.convert_to_element response
assert_equal 'test-element-id', e.id
assert_equal ::Appium::Core::Element, e.class
assert_equal 'sample text', e.cached.text
end
end
end