From c8548774d90eee7b05e5f5eafcb3dbc2270d1a6f Mon Sep 17 00:00:00 2001 From: Alex Rodionov Date: Mon, 29 Mar 2021 19:21:24 -0700 Subject: [PATCH] Allow being explicit about alwaysMatch/firstMatch capabilities It's now possible to explicitly specify if your desired capabilities should be sent as alwaysMatch/firstMatch capabilities [1]. The older implementation defaulted to always use firstMatch which is enough for most cases. Some examples of usage can be found in the capabilities specs. Fixes #9344 [1]: https://www.w3.org/TR/webdriver/#new-session --- rb/lib/selenium/webdriver/remote/bridge.rb | 7 +- .../selenium/webdriver/remote/capabilities.rb | 86 +++++++++++++------ .../selenium/webdriver/remote/bridge_spec.rb | 35 ++++++++ .../webdriver/remote/capabilities_spec.rb | 17 +++- 4 files changed, 115 insertions(+), 30 deletions(-) diff --git a/rb/lib/selenium/webdriver/remote/bridge.rb b/rb/lib/selenium/webdriver/remote/bridge.rb index 18cdd5f57b491..dfbd6b285f2cf 100644 --- a/rb/lib/selenium/webdriver/remote/bridge.rb +++ b/rb/lib/selenium/webdriver/remote/bridge.rb @@ -49,7 +49,7 @@ def initialize(url:, http_client: nil) # def create_session(capabilities) - response = execute(:new_session, {}, {capabilities: {firstMatch: [capabilities]}}) + response = execute(:new_session, {}, prepare_capabilities_payload(capabilities)) @session_id = response['sessionId'] capabilities = response['capabilities'] @@ -594,6 +594,11 @@ def element_id_from(id) id['ELEMENT'] || id['element-6066-11e4-a52e-4f735466cecf'] end + def prepare_capabilities_payload(capabilities) + capabilities = {firstMatch: [capabilities]} if !capabilities['alwaysMatch'] && !capabilities['firstMatch'] + {capabilities: capabilities} + end + def convert_locator(how, what) how = SearchContext::FINDERS[how.to_sym] || how diff --git a/rb/lib/selenium/webdriver/remote/capabilities.rb b/rb/lib/selenium/webdriver/remote/capabilities.rb index 5301732e03f89..cf86388912a9a 100755 --- a/rb/lib/selenium/webdriver/remote/capabilities.rb +++ b/rb/lib/selenium/webdriver/remote/capabilities.rb @@ -121,6 +121,14 @@ def internet_explorer(opts = {}) end alias_method :ie, :internet_explorer + def always_match(capabilities) + new(always_match: capabilities) + end + + def first_match(*capabilities) + new(first_match: capabilities) + end + # # @api private # @@ -179,8 +187,9 @@ def process_timeouts(caps, timeouts) # def initialize(opts = {}) - @capabilities = opts - self.proxy = opts.delete(:proxy) + @capabilities = {} + self.proxy = opts.delete(:proxy) if opts[:proxy] + @capabilities.merge!(opts) end # @@ -221,28 +230,9 @@ def proxy=(proxy) # def as_json(*) - hash = {} - - @capabilities.each do |key, value| - case key - when :platform - hash['platform'] = value.to_s.upcase - when :proxy - next unless value - - process_proxy(hash, value) - when :unhandled_prompt_behavior - hash['unhandledPromptBehavior'] = value.is_a?(Symbol) ? value.to_s.tr('_', ' ') : value - when String - hash[key.to_s] = value - when Symbol - hash[self.class.camel_case(key)] = value - else - raise TypeError, "expected String or Symbol, got #{key.inspect}:#{key.class} / #{value.inspect}" - end + @capabilities.each_with_object({}) do |(key, value), hash| + hash[convert_key(key)] = process_capabilities(key, value, hash) end - - hash end def to_json(*) @@ -263,13 +253,53 @@ def ==(other) private - def process_proxy(hash, value) - hash['proxy'] = value.as_json - hash['proxy']['proxyType'] &&= hash['proxy']['proxyType'].downcase + def process_capabilities(key, value, hash) + case value + when Array + value.map { |v| process_capabilities(key, v, hash) } + when Hash + value.each_with_object({}) do |(k, v), h| + h[convert_key(k)] = process_capabilities(k, v, h) + end + when Capabilities, Options + value.as_json + else + convert_value(key, value) + end + end + + def convert_key(key) + case key + when String + key.to_s + when Symbol + self.class.camel_case(key) + else + raise TypeError, "expected String or Symbol, got #{key.inspect}:#{key.class}" + end + end - return unless hash['proxy']['noProxy'].is_a?(String) + def convert_value(key, value) + case key + when :platform + value.to_s.upcase + when :proxy + convert_proxy(value) + when :unhandled_prompt_behavior + value.is_a?(Symbol) ? value.to_s.tr('_', ' ') : value + else + value + end + end + + def convert_proxy(value) + return unless value - hash['proxy']['noProxy'] = hash['proxy']['noProxy'].split(', ') + hash = value.as_json + hash['proxyType'] &&= hash['proxyType'].downcase + hash['noProxy'] = hash['noProxy'].split(', ') if hash['noProxy'].is_a?(String) + + hash end end # Capabilities end # Remote diff --git a/rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb b/rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb index aee4d818bea17..d52de1821097b 100644 --- a/rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb +++ b/rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb @@ -71,6 +71,41 @@ module Remote expect(http).to have_received(:request).with(any_args, payload) end + it 'uses alwaysMatch when passed' do + payload = JSON.generate( + capabilities: { + alwaysMatch: { + browserName: 'chrome' + } + } + ) + + allow(http).to receive(:request) + .with(any_args, payload) + .and_return('status' => 200, 'value' => {'sessionId' => 'foo', 'capabilities' => {}}) + + bridge.create_session(Capabilities.always_match(Capabilities.chrome).as_json) + expect(http).to have_received(:request).with(any_args, payload) + end + + it 'uses firstMatch when passed' do + payload = JSON.generate( + capabilities: { + firstMatch: [ + {browserName: 'chrome'}, + {browserName: 'firefox'} + ] + } + ) + + allow(http).to receive(:request) + .with(any_args, payload) + .and_return('status' => 200, 'value' => {'sessionId' => 'foo', 'capabilities' => {}}) + + bridge.create_session(Capabilities.first_match(Capabilities.chrome, Capabilities.firefox).as_json) + expect(http).to have_received(:request).with(any_args, payload) + end + it 'supports responses with "value" -> "capabilities" capabilities' do allow(http).to receive(:request) .and_return('value' => {'sessionId' => '', 'capabilities' => {'browserName' => 'firefox'}}) diff --git a/rb/spec/unit/selenium/webdriver/remote/capabilities_spec.rb b/rb/spec/unit/selenium/webdriver/remote/capabilities_spec.rb index a3021d6023d44..04b43a387023f 100644 --- a/rb/spec/unit/selenium/webdriver/remote/capabilities_spec.rb +++ b/rb/spec/unit/selenium/webdriver/remote/capabilities_spec.rb @@ -61,7 +61,7 @@ module Remote end it 'should default to no proxy' do - expect(Capabilities.new.proxy).to be_nil + expect { Capabilities.new.proxy }.to raise_error(KeyError) end it 'can set and get standard capabilities' do @@ -110,6 +110,21 @@ module Remote caps = Capabilities.new(browser_name: 'firefox', 'extension:customCapability': true) expect(caps).to eq(Capabilities.json_create(caps.as_json)) end + + it 'allows to set alwaysMatch' do + expected = {'alwaysMatch' => {'browserName' => 'chrome'}} + expect(Capabilities.always_match(browser_name: 'chrome').as_json).to eq(expected) + expect(Capabilities.always_match('browserName' => 'chrome').as_json).to eq(expected) + expect(Capabilities.always_match(Capabilities.chrome).as_json).to eq(expected) + end + + it 'allows to set firstMatch' do + expected = {'firstMatch' => [{'browserName' => 'chrome'}, {'browserName' => 'firefox'}]} + expect(Capabilities.first_match({browser_name: 'chrome'}, {browser_name: 'firefox'}).as_json).to eq(expected) + expect(Capabilities.first_match({'browserName' => 'chrome'}, + {'browserName' => 'firefox'}).as_json).to eq(expected) + expect(Capabilities.first_match(Capabilities.chrome, Capabilities.firefox).as_json).to eq(expected) + end end end # Remote end # WebDriver