diff --git a/lib/appium_lib_core/common/base/bridge.rb b/lib/appium_lib_core/common/base/bridge.rb index 48aefc3f..52398d0a 100644 --- a/lib/appium_lib_core/common/base/bridge.rb +++ b/lib/appium_lib_core/common/base/bridge.rb @@ -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 diff --git a/lib/appium_lib_core/element.rb b/lib/appium_lib_core/element.rb index 2fefb228..6cee859b 100644 --- a/lib/appium_lib_core/element.rb +++ b/lib/appium_lib_core/element.rb @@ -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 @@ -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 diff --git a/lib/appium_lib_core/support/event_firing_bridge.rb b/lib/appium_lib_core/support/event_firing_bridge.rb index b9751484..de784841 100644 --- a/lib/appium_lib_core/support/event_firing_bridge.rb +++ b/lib/appium_lib_core/support/event_firing_bridge.rb @@ -29,7 +29,7 @@ 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) @@ -37,7 +37,7 @@ def find_elements_by(how, what, parent = nil) @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 diff --git a/sig/lib/appium_lib_core/common/base/bridge.rbs b/sig/lib/appium_lib_core/common/base/bridge.rbs index 60acb8fe..845b618e 100644 --- a/sig/lib/appium_lib_core/common/base/bridge.rbs +++ b/sig/lib/appium_lib_core/common/base/bridge.rbs @@ -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] diff --git a/sig/lib/appium_lib_core/element.rbs b/sig/lib/appium_lib_core/element.rbs index 7a76c0dd..8ed7fb11 100644 --- a/sig/lib/appium_lib_core/element.rbs +++ b/sig/lib/appium_lib_core/element.rbs @@ -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 diff --git a/test/unit/common/element_test.rb b/test/unit/common/element_test.rb index 4ec0030f..f4f246b8 100644 --- a/test/unit/common/element_test.rb +++ b/test/unit/common/element_test.rb @@ -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 diff --git a/test/unit/driver_test.rb b/test/unit/driver_test.rb index 44c397d2..e17f5ba4 100644 --- a/test/unit/driver_test.rb +++ b/test/unit/driver_test.rb @@ -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