Skip to content

Commit c854877

Browse files
committed
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
1 parent eaa1047 commit c854877

File tree

4 files changed

+115
-30
lines changed

4 files changed

+115
-30
lines changed

rb/lib/selenium/webdriver/remote/bridge.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def initialize(url:, http_client: nil)
4949
#
5050

5151
def create_session(capabilities)
52-
response = execute(:new_session, {}, {capabilities: {firstMatch: [capabilities]}})
52+
response = execute(:new_session, {}, prepare_capabilities_payload(capabilities))
5353

5454
@session_id = response['sessionId']
5555
capabilities = response['capabilities']
@@ -594,6 +594,11 @@ def element_id_from(id)
594594
id['ELEMENT'] || id['element-6066-11e4-a52e-4f735466cecf']
595595
end
596596

597+
def prepare_capabilities_payload(capabilities)
598+
capabilities = {firstMatch: [capabilities]} if !capabilities['alwaysMatch'] && !capabilities['firstMatch']
599+
{capabilities: capabilities}
600+
end
601+
597602
def convert_locator(how, what)
598603
how = SearchContext::FINDERS[how.to_sym] || how
599604

rb/lib/selenium/webdriver/remote/capabilities.rb

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,14 @@ def internet_explorer(opts = {})
121121
end
122122
alias_method :ie, :internet_explorer
123123

124+
def always_match(capabilities)
125+
new(always_match: capabilities)
126+
end
127+
128+
def first_match(*capabilities)
129+
new(first_match: capabilities)
130+
end
131+
124132
#
125133
# @api private
126134
#
@@ -179,8 +187,9 @@ def process_timeouts(caps, timeouts)
179187
#
180188

181189
def initialize(opts = {})
182-
@capabilities = opts
183-
self.proxy = opts.delete(:proxy)
190+
@capabilities = {}
191+
self.proxy = opts.delete(:proxy) if opts[:proxy]
192+
@capabilities.merge!(opts)
184193
end
185194

186195
#
@@ -221,28 +230,9 @@ def proxy=(proxy)
221230
#
222231

223232
def as_json(*)
224-
hash = {}
225-
226-
@capabilities.each do |key, value|
227-
case key
228-
when :platform
229-
hash['platform'] = value.to_s.upcase
230-
when :proxy
231-
next unless value
232-
233-
process_proxy(hash, value)
234-
when :unhandled_prompt_behavior
235-
hash['unhandledPromptBehavior'] = value.is_a?(Symbol) ? value.to_s.tr('_', ' ') : value
236-
when String
237-
hash[key.to_s] = value
238-
when Symbol
239-
hash[self.class.camel_case(key)] = value
240-
else
241-
raise TypeError, "expected String or Symbol, got #{key.inspect}:#{key.class} / #{value.inspect}"
242-
end
233+
@capabilities.each_with_object({}) do |(key, value), hash|
234+
hash[convert_key(key)] = process_capabilities(key, value, hash)
243235
end
244-
245-
hash
246236
end
247237

248238
def to_json(*)
@@ -263,13 +253,53 @@ def ==(other)
263253

264254
private
265255

266-
def process_proxy(hash, value)
267-
hash['proxy'] = value.as_json
268-
hash['proxy']['proxyType'] &&= hash['proxy']['proxyType'].downcase
256+
def process_capabilities(key, value, hash)
257+
case value
258+
when Array
259+
value.map { |v| process_capabilities(key, v, hash) }
260+
when Hash
261+
value.each_with_object({}) do |(k, v), h|
262+
h[convert_key(k)] = process_capabilities(k, v, h)
263+
end
264+
when Capabilities, Options
265+
value.as_json
266+
else
267+
convert_value(key, value)
268+
end
269+
end
270+
271+
def convert_key(key)
272+
case key
273+
when String
274+
key.to_s
275+
when Symbol
276+
self.class.camel_case(key)
277+
else
278+
raise TypeError, "expected String or Symbol, got #{key.inspect}:#{key.class}"
279+
end
280+
end
269281

270-
return unless hash['proxy']['noProxy'].is_a?(String)
282+
def convert_value(key, value)
283+
case key
284+
when :platform
285+
value.to_s.upcase
286+
when :proxy
287+
convert_proxy(value)
288+
when :unhandled_prompt_behavior
289+
value.is_a?(Symbol) ? value.to_s.tr('_', ' ') : value
290+
else
291+
value
292+
end
293+
end
294+
295+
def convert_proxy(value)
296+
return unless value
271297

272-
hash['proxy']['noProxy'] = hash['proxy']['noProxy'].split(', ')
298+
hash = value.as_json
299+
hash['proxyType'] &&= hash['proxyType'].downcase
300+
hash['noProxy'] = hash['noProxy'].split(', ') if hash['noProxy'].is_a?(String)
301+
302+
hash
273303
end
274304
end # Capabilities
275305
end # Remote

rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,41 @@ module Remote
7171
expect(http).to have_received(:request).with(any_args, payload)
7272
end
7373

74+
it 'uses alwaysMatch when passed' do
75+
payload = JSON.generate(
76+
capabilities: {
77+
alwaysMatch: {
78+
browserName: 'chrome'
79+
}
80+
}
81+
)
82+
83+
allow(http).to receive(:request)
84+
.with(any_args, payload)
85+
.and_return('status' => 200, 'value' => {'sessionId' => 'foo', 'capabilities' => {}})
86+
87+
bridge.create_session(Capabilities.always_match(Capabilities.chrome).as_json)
88+
expect(http).to have_received(:request).with(any_args, payload)
89+
end
90+
91+
it 'uses firstMatch when passed' do
92+
payload = JSON.generate(
93+
capabilities: {
94+
firstMatch: [
95+
{browserName: 'chrome'},
96+
{browserName: 'firefox'}
97+
]
98+
}
99+
)
100+
101+
allow(http).to receive(:request)
102+
.with(any_args, payload)
103+
.and_return('status' => 200, 'value' => {'sessionId' => 'foo', 'capabilities' => {}})
104+
105+
bridge.create_session(Capabilities.first_match(Capabilities.chrome, Capabilities.firefox).as_json)
106+
expect(http).to have_received(:request).with(any_args, payload)
107+
end
108+
74109
it 'supports responses with "value" -> "capabilities" capabilities' do
75110
allow(http).to receive(:request)
76111
.and_return('value' => {'sessionId' => '', 'capabilities' => {'browserName' => 'firefox'}})

rb/spec/unit/selenium/webdriver/remote/capabilities_spec.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ module Remote
6161
end
6262

6363
it 'should default to no proxy' do
64-
expect(Capabilities.new.proxy).to be_nil
64+
expect { Capabilities.new.proxy }.to raise_error(KeyError)
6565
end
6666

6767
it 'can set and get standard capabilities' do
@@ -110,6 +110,21 @@ module Remote
110110
caps = Capabilities.new(browser_name: 'firefox', 'extension:customCapability': true)
111111
expect(caps).to eq(Capabilities.json_create(caps.as_json))
112112
end
113+
114+
it 'allows to set alwaysMatch' do
115+
expected = {'alwaysMatch' => {'browserName' => 'chrome'}}
116+
expect(Capabilities.always_match(browser_name: 'chrome').as_json).to eq(expected)
117+
expect(Capabilities.always_match('browserName' => 'chrome').as_json).to eq(expected)
118+
expect(Capabilities.always_match(Capabilities.chrome).as_json).to eq(expected)
119+
end
120+
121+
it 'allows to set firstMatch' do
122+
expected = {'firstMatch' => [{'browserName' => 'chrome'}, {'browserName' => 'firefox'}]}
123+
expect(Capabilities.first_match({browser_name: 'chrome'}, {browser_name: 'firefox'}).as_json).to eq(expected)
124+
expect(Capabilities.first_match({'browserName' => 'chrome'},
125+
{'browserName' => 'firefox'}).as_json).to eq(expected)
126+
expect(Capabilities.first_match(Capabilities.chrome, Capabilities.firefox).as_json).to eq(expected)
127+
end
113128
end
114129
end # Remote
115130
end # WebDriver

0 commit comments

Comments
 (0)