diff --git a/lib/appium_lib_core/common/base/bridge.rb b/lib/appium_lib_core/common/base/bridge.rb index a6666d27..a68561e6 100644 --- a/lib/appium_lib_core/common/base/bridge.rb +++ b/lib/appium_lib_core/common/base/bridge.rb @@ -47,8 +47,6 @@ class Bridge < ::Selenium::WebDriver::Remote::Bridge # No 'browserName' means the session is native appium connection APPIUM_NATIVE_BROWSER_NAME = 'appium' - attr_reader :available_commands - def browser @browser ||= begin name = @capabilities&.browser_name @@ -74,7 +72,6 @@ def browser # ) # def attach_to(session_id, platform_name, automation_name) - @available_commands = ::Appium::Core::Commands::COMMANDS.dup @session_id = session_id # generate a dummy capabilities instance which only has the given platformName and automationName @@ -109,8 +106,6 @@ def attach_to(session_id, platform_name, automation_name) # driver = core.start_driver # def create_session(capabilities) - @available_commands = ::Appium::Core::Commands::COMMANDS.dup - always_match = add_appium_prefix(capabilities) response = execute(:new_session, {}, { capabilities: { alwaysMatch: always_match, firstMatch: [{}] } }) # steep:ignore @@ -162,29 +157,16 @@ def json_create(value) public - # command for Appium 2.0. - - # Example: - # driver.add_command(name: :available_contexts, method: :get, url: 'session/:session_id/contexts') do - # execute(:available_contexts, {}) || [] - # end - # Then, - # driver.available_contexts #=> ["NATIVE_APP"] - - # def add_command(method:, url:, name:, &block) - # Bridge.add_command name, method, url, &block - # end - - def add_command(method:, url:, name:, &block) - ::Appium::Logger.info "Overriding the method '#{name}' for '#{url}'" if @available_commands.key? name - - @available_commands[name] = [method, url] - - ::Appium::Core::Device.add_endpoint_method name, &block + # Override Selenium's command_list to use Appium's command definitions + # instead of the default W3C COMMANDS from Selenium. + def command_list + ::Appium::Core::Commands::COMMANDS end + # Override Selenium's commands to resolve extra_commands from this subclass + # rather than the parent Selenium::WebDriver::Remote::Bridge. def commands(command) - @available_commands[command] || Bridge.extra_commands[command] + command_list[command] || self.class.extra_commands&.[](command) end def status diff --git a/lib/appium_lib_core/common/base/driver.rb b/lib/appium_lib_core/common/base/driver.rb index cfc8946f..54476ae0 100644 --- a/lib/appium_lib_core/common/base/driver.rb +++ b/lib/appium_lib_core/common/base/driver.rb @@ -135,25 +135,21 @@ def update_sending_request_to(protocol:, host:, port:, path:) # drivers/plugins in Appium 2.0. Appium 2.0 and its custom drivers/plugins allow you # to define custom commands that are not part of W3C spec. # + # Uses Selenium's +Bridge.add_command+ under the hood to register the command + # and define the method on the bridge. + # # @param [Symbol] method HTTP request method as https://www.w3.org/TR/webdriver/#endpoints # @param [string] url The url to URL template as https://www.w3.org/TR/webdriver/#endpoints. # +:session_id+ is the placeholder of 'session id'. # Other place holders can be specified with +:+ prefix like +:id+. # Then, the +:id+ will be replaced with a given value as the seconds argument of +execute+ # @param [Symbol] name The name of method that is called as the driver instance method. - # @param [Proc] block The block to involve as the method. - # Please define a method that has the same +name+ with arguments you want. - # The method must has +execute+ method. tHe +execute+ is calls the +url+ - # with the given parameters. - # The first argument should be +name+ as symbol. - # The second argument should be hash. If keys in the hash matches +:+ prefix - # string in the given url, the matched string in the given url will be - # values in the hash. - # The third argument should be hash. The hash will be the request body. - # Please read examples below for more details. + # @param [Proc] block The block becomes the method body. It is executed in the context of + # the bridge instance, so +execute+ is available. When no block is given, + # a default implementation calling +execute(name)+ is used. # @raise [ArgumentError] If the given +method+ is invalid value. # - # @example + # @example Simple GET command (no block) # # @driver.add_command( # method: :get, @@ -163,30 +159,37 @@ def update_sending_request_to(protocol:, host:, port:, path:) # # Send a GET request to 'session//path/to/custom/url' # @driver.test_command # + # @example POST command with arguments (do/end block) # # @driver.add_command( # method: :post, # url: 'session/:session_id/path/to/custom/url', # name: :test_command - # ) do - # def test_command(argument) - # execute(:test_command, {}, { dummy: argument }) - # end + # ) do |argument| + # execute(:test_command, {}, { dummy: argument }) # end # # Send a POST request to 'session//path/to/custom/url' # # with body "{ dummy: 1 }" as JSON object. "1" is the argument. # # ':session_id' in the given 'url' is replaced with current 'session id'. # @driver.test_command(1) # + # @example POST command with arguments (inline block) + # + # @driver.add_command( + # method: :post, + # url: 'session/:session_id/path/to/custom/url', + # name: :test_command + # ) { |argument| execute(:test_command, {}, { dummy: argument }) } + # @driver.test_command(1) + # + # @example POST command with URL placeholders (do/end block) # # @driver.add_command( # method: :post, # url: 'session/:session_id/element/:id/custom/action', # name: :test_action_command - # ) do - # def test_action_command(element_id, action) - # execute(:test_action_command, {id: element_id}, { dummy_action: action }) - # end + # ) do |element_id, action| + # execute(:test_action_command, {id: element_id}, { dummy_action: action }) # end # # Send a POST request to 'session//element//custom/action' # # with body "{ dummy_action: #{action} }" as JSON object. "action" is the seconds argument. @@ -195,10 +198,28 @@ def update_sending_request_to(protocol:, host:, port:, path:) # e = @driver.find_element :accessibility_id, 'an element' # @driver.test_action_command(e.id, 'action') # + # @example POST command with URL placeholders (inline block) + # + # @driver.add_command( + # method: :post, + # url: 'session/:session_id/element/:id/custom/action', + # name: :test_action_command + # ) { |element_id, action| execute(:test_action_command, {id: element_id}, { dummy_action: action }) } + # e = @driver.find_element :accessibility_id, 'an element' + # @driver.test_action_command(e.id, 'action') + # def add_command(method:, url:, name:, &block) raise ::Appium::Core::Error::ArgumentError, "Available method is either #{AVAILABLE_METHODS}" unless AVAILABLE_METHODS.include? method - @bridge.add_command method: method, url: url, name: name, &block + ::Appium::Logger.info "Overriding the command '#{name}' for '#{url}'" if Bridge.extra_commands&.key?(name) + + # Use Selenium's Bridge.add_command to register the command and define the method. + # When no block is given, create a default implementation that calls execute. + block ||= proc { execute(name) } + Bridge.add_command(name, method, url, &block) + + # Ensure the driver delegates the new method to bridge + self.class.class_eval { def_delegator :@bridge, name } unless self.class.method_defined?(name) end ### Methods for Appium diff --git a/sig/lib/appium_lib_core/common/base/bridge.rbs b/sig/lib/appium_lib_core/common/base/bridge.rbs index 79216666..ae65473a 100644 --- a/sig/lib/appium_lib_core/common/base/bridge.rbs +++ b/sig/lib/appium_lib_core/common/base/bridge.rbs @@ -10,8 +10,6 @@ module Appium @browser: untyped - @available_commands: untyped - @session_id: untyped # generate a dummy capabilities instance which only has the given platformName and automationName @@ -51,8 +49,6 @@ module Appium # No 'browserName' means the session is native appium connection APPIUM_NATIVE_BROWSER_NAME: "appium" - attr_reader available_commands: untyped - def browser: () -> untyped # Appium only. @@ -115,8 +111,10 @@ module Appium public - def add_command: (method: untyped, url: untyped, name: untyped) { (?) -> untyped } -> untyped + # Override Selenium's command_list to use Appium's command definitions + def command_list: () -> Hash[Symbol, Array[Symbol | String]] + # Override Selenium's commands to resolve extra_commands from this subclass def commands: (untyped command) -> untyped def status: () -> untyped diff --git a/test/unit/android/webdriver/w3c/commands_test.rb b/test/unit/android/webdriver/w3c/commands_test.rb index bf9d9bfb..df77fd9c 100644 --- a/test/unit/android/webdriver/w3c/commands_test.rb +++ b/test/unit/android/webdriver/w3c/commands_test.rb @@ -50,10 +50,8 @@ def test_add_command_block method: :post, url: 'session/:session_id/path/to/custom/url', name: :test_command - ) do - def test_command(argument) - execute(:test_command, {}, { dummy: argument }) - end + ) do |argument| + execute(:test_command, {}, { dummy: argument }) end assert_equal @driver.respond_to?(:test_command), true @@ -72,10 +70,8 @@ def test_add_command_block_element_id method: :post, url: 'session/:session_id/path/to/custom/:element_id/url', name: :test_command - ) do - def test_command(argument) - execute(:test_command, { element_id: 'dummy_element_id' }, { dummy: argument }) - end + ) do |argument| + execute(:test_command, { element_id: 'dummy_element_id' }, { dummy: argument }) end assert_equal @driver.respond_to?(:test_command), true