Skip to content

Commit

Permalink
Allow being explicit about alwaysMatch/firstMatch capabilities
Browse files Browse the repository at this point in the history
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
  • Loading branch information
p0deje committed Mar 30, 2021
1 parent eaa1047 commit c854877
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 30 deletions.
7 changes: 6 additions & 1 deletion rb/lib/selenium/webdriver/remote/bridge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -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

Expand Down
86 changes: 58 additions & 28 deletions rb/lib/selenium/webdriver/remote/capabilities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down Expand Up @@ -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

#
Expand Down Expand Up @@ -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(*)
Expand All @@ -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
Expand Down
35 changes: 35 additions & 0 deletions rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'}})
Expand Down
17 changes: 16 additions & 1 deletion rb/spec/unit/selenium/webdriver/remote/capabilities_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit c854877

Please sign in to comment.