Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/rb/lib/selenium/webdriver/remote/bridge.rb
1865 views
1
# frozen_string_literal: true
2
3
# Licensed to the Software Freedom Conservancy (SFC) under one
4
# or more contributor license agreements. See the NOTICE file
5
# distributed with this work for additional information
6
# regarding copyright ownership. The SFC licenses this file
7
# to you under the Apache License, Version 2.0 (the
8
# "License"); you may not use this file except in compliance
9
# with the License. You may obtain a copy of the License at
10
#
11
# http://www.apache.org/licenses/LICENSE-2.0
12
#
13
# Unless required by applicable law or agreed to in writing,
14
# software distributed under the License is distributed on an
15
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
# KIND, either express or implied. See the License for the
17
# specific language governing permissions and limitations
18
# under the License.
19
20
module Selenium
21
module WebDriver
22
module Remote
23
class Bridge
24
autoload :COMMANDS, 'selenium/webdriver/remote/bridge/commands'
25
autoload :LocatorConverter, 'selenium/webdriver/remote/bridge/locator_converter'
26
27
include Atoms
28
29
PORT = 4444
30
31
attr_accessor :http, :file_detector
32
attr_reader :capabilities
33
34
class << self
35
attr_reader :extra_commands
36
attr_writer :element_class, :locator_converter
37
38
def add_command(name, verb, url, &block)
39
@extra_commands ||= {}
40
@extra_commands[name] = [verb, url]
41
define_method(name, &block)
42
end
43
44
def locator_converter
45
@locator_converter ||= LocatorConverter.new
46
end
47
48
def element_class
49
@element_class ||= Element
50
end
51
end
52
53
#
54
# Initializes the bridge with the given server URL
55
# @param [String, URI] url url for the remote server
56
# @param [Object] http_client an HTTP client instance that implements the same protocol as Http::Default
57
# @api private
58
#
59
60
def initialize(url:, http_client: nil)
61
uri = url.is_a?(URI) ? url : URI.parse(url)
62
uri.path += '/' unless uri.path.end_with?('/')
63
64
@http = http_client || Http::Default.new
65
@http.server_url = uri
66
@file_detector = nil
67
68
@locator_converter = self.class.locator_converter
69
end
70
71
#
72
# Creates session.
73
#
74
75
def create_session(capabilities)
76
response = execute(:new_session, {}, prepare_capabilities_payload(capabilities))
77
78
@session_id = response['sessionId']
79
capabilities = response['capabilities']
80
81
raise Error::WebDriverError, 'no sessionId in returned payload' unless @session_id
82
83
@capabilities = Capabilities.json_create(capabilities)
84
85
case @capabilities[:browser_name]
86
when 'chrome', 'chrome-headless-shell'
87
extend(WebDriver::Chrome::Features)
88
when 'firefox'
89
extend(WebDriver::Firefox::Features)
90
when 'msedge', 'MicrosoftEdge'
91
extend(WebDriver::Edge::Features)
92
when 'Safari', 'Safari Technology Preview'
93
extend(WebDriver::Safari::Features)
94
when 'internet explorer'
95
extend(WebDriver::IE::Features)
96
end
97
end
98
99
#
100
# Returns the current session ID.
101
#
102
103
def session_id
104
@session_id || raise(Error::WebDriverError, 'no current session exists')
105
end
106
107
def browser
108
@browser ||= begin
109
name = @capabilities.browser_name
110
name ? name.tr(' -', '_').downcase.to_sym : 'unknown'
111
end
112
end
113
114
def status
115
execute :status
116
end
117
118
def get(url)
119
execute :get, {}, {url: url}
120
end
121
122
#
123
# timeouts
124
#
125
126
def timeouts
127
execute :get_timeouts, {}
128
end
129
130
def timeouts=(timeouts)
131
execute :set_timeout, {}, timeouts
132
end
133
134
#
135
# alerts
136
#
137
138
def accept_alert
139
execute :accept_alert
140
end
141
142
def dismiss_alert
143
execute :dismiss_alert
144
end
145
146
def alert=(keys)
147
execute :send_alert_text, {}, {value: keys.chars, text: keys}
148
end
149
150
def alert_text
151
execute :get_alert_text
152
end
153
154
#
155
# navigation
156
#
157
158
def go_back
159
execute :back
160
end
161
162
def go_forward
163
execute :forward
164
end
165
166
def url
167
execute :get_current_url
168
end
169
170
def title
171
execute :get_title
172
end
173
174
def page_source
175
execute :get_page_source
176
end
177
178
#
179
# Create a new top-level browsing context
180
# https://w3c.github.io/webdriver/#new-window
181
# @param type [String] Supports two values: 'tab' and 'window'.
182
# Use 'tab' if you'd like the new window to share an OS-level window
183
# with the current browsing context.
184
# Use 'window' otherwise
185
# @return [Hash] Containing 'handle' with the value of the window handle
186
# and 'type' with the value of the created window type
187
#
188
def new_window(type)
189
execute :new_window, {}, {type: type}
190
end
191
192
def switch_to_window(name)
193
execute :switch_to_window, {}, {handle: name}
194
end
195
196
def switch_to_frame(id)
197
id = find_element_by('id', id) if id.is_a? String
198
execute :switch_to_frame, {}, {id: id}
199
end
200
201
def switch_to_parent_frame
202
execute :switch_to_parent_frame
203
end
204
205
def switch_to_default_content
206
switch_to_frame nil
207
end
208
209
QUIT_ERRORS = [IOError].freeze
210
211
def quit
212
execute :delete_session
213
http.close
214
rescue *QUIT_ERRORS
215
nil
216
end
217
218
def close
219
execute :close_window
220
end
221
222
def refresh
223
execute :refresh
224
end
225
226
#
227
# window handling
228
#
229
230
def window_handles
231
execute :get_window_handles
232
end
233
234
def window_handle
235
execute :get_window_handle
236
end
237
238
def resize_window(width, height, handle = :current)
239
raise Error::WebDriverError, 'Switch to desired window before changing its size' unless handle == :current
240
241
set_window_rect(width: width, height: height)
242
end
243
244
def window_size(handle = :current)
245
unless handle == :current
246
raise Error::UnsupportedOperationError,
247
'Switch to desired window before getting its size'
248
end
249
250
data = execute :get_window_rect
251
Dimension.new data['width'], data['height']
252
end
253
254
def minimize_window
255
execute :minimize_window
256
end
257
258
def maximize_window(handle = :current)
259
unless handle == :current
260
raise Error::UnsupportedOperationError,
261
'Switch to desired window before changing its size'
262
end
263
264
execute :maximize_window
265
end
266
267
def full_screen_window
268
execute :fullscreen_window
269
end
270
271
def reposition_window(x, y)
272
set_window_rect(x: x, y: y)
273
end
274
275
def window_position
276
data = execute :get_window_rect
277
Point.new data['x'], data['y']
278
end
279
280
def set_window_rect(x: nil, y: nil, width: nil, height: nil)
281
params = {x: x, y: y, width: width, height: height}
282
params.update(params) { |_k, v| Integer(v) unless v.nil? }
283
execute :set_window_rect, {}, params
284
end
285
286
def window_rect
287
data = execute :get_window_rect
288
Rectangle.new data['x'], data['y'], data['width'], data['height']
289
end
290
291
def screenshot
292
execute :take_screenshot
293
end
294
295
def element_screenshot(element)
296
execute :take_element_screenshot, id: element
297
end
298
299
#
300
# javascript execution
301
#
302
303
def execute_script(script, *args)
304
result = execute :execute_script, {}, {script: script, args: args}
305
unwrap_script_result result
306
end
307
308
def execute_async_script(script, *args)
309
result = execute :execute_async_script, {}, {script: script, args: args}
310
unwrap_script_result result
311
end
312
313
#
314
# cookies
315
#
316
317
def manage
318
@manage ||= WebDriver::Manager.new(self)
319
end
320
321
def add_cookie(cookie)
322
execute :add_cookie, {}, {cookie: cookie}
323
end
324
325
def delete_cookie(name)
326
raise ArgumentError, 'Cookie name cannot be null or empty' if name.nil? || name.to_s.strip.empty?
327
328
execute :delete_cookie, name: name
329
end
330
331
def cookie(name)
332
execute :get_cookie, name: name
333
end
334
335
def cookies
336
execute :get_all_cookies
337
end
338
339
def delete_all_cookies
340
execute :delete_all_cookies
341
end
342
343
#
344
# actions
345
#
346
347
def action(async: false, devices: [], duration: 250)
348
ActionBuilder.new self, async: async, devices: devices, duration: duration
349
end
350
alias actions action
351
352
def send_actions(data)
353
execute :actions, {}, {actions: data}
354
end
355
356
def release_actions
357
execute :release_actions
358
end
359
360
def print_page(options = {})
361
execute :print_page, {}, {options: options}
362
end
363
364
def click_element(element)
365
execute :element_click, id: element
366
end
367
368
def send_keys_to_element(element, keys)
369
keys = upload_if_necessary(keys) if @file_detector
370
text = keys.join
371
execute :element_send_keys, {id: element}, {value: text.chars, text: text}
372
end
373
374
def clear_element(element)
375
execute :element_clear, id: element
376
end
377
378
def submit_element(element)
379
script = "/* submitForm */ var form = arguments[0];\n" \
380
"while (form.nodeName != \"FORM\" && form.parentNode) {\n " \
381
"form = form.parentNode;\n" \
382
"}\n" \
383
"if (!form) { throw Error('Unable to find containing form element'); }\n" \
384
"if (!form.ownerDocument) { throw Error('Unable to find owning document'); }\n" \
385
"var e = form.ownerDocument.createEvent('Event');\n" \
386
"e.initEvent('submit', true, true);\n" \
387
"if (form.dispatchEvent(e)) { HTMLFormElement.prototype.submit.call(form) }\n"
388
389
execute_script(script, Bridge.element_class::ELEMENT_KEY => element)
390
rescue Error::JavascriptError
391
raise Error::UnsupportedOperationError, 'To submit an element, it must be nested inside a form element'
392
end
393
394
#
395
# element properties
396
#
397
398
def element_tag_name(element)
399
execute :get_element_tag_name, id: element
400
end
401
402
def element_attribute(element, name)
403
WebDriver.logger.debug "Using script for :getAttribute of #{name}", id: :script
404
execute_atom :getAttribute, element, name
405
end
406
407
def element_dom_attribute(element, name)
408
execute :get_element_attribute, id: element, name: name
409
end
410
411
def element_property(element, name)
412
execute :get_element_property, id: element, name: name
413
end
414
415
def element_aria_role(element)
416
execute :get_element_aria_role, id: element
417
end
418
419
def element_aria_label(element)
420
execute :get_element_aria_label, id: element
421
end
422
423
def element_value(element)
424
element_property element, 'value'
425
end
426
427
def element_text(element)
428
execute :get_element_text, id: element
429
end
430
431
def element_location(element)
432
data = execute :get_element_rect, id: element
433
434
Point.new data['x'], data['y']
435
end
436
437
def element_rect(element)
438
data = execute :get_element_rect, id: element
439
440
Rectangle.new data['x'], data['y'], data['width'], data['height']
441
end
442
443
def element_location_once_scrolled_into_view(element)
444
send_keys_to_element(element, [''])
445
element_location(element)
446
end
447
448
def element_size(element)
449
data = execute :get_element_rect, id: element
450
451
Dimension.new data['width'], data['height']
452
end
453
454
def element_enabled?(element)
455
execute :is_element_enabled, id: element
456
end
457
458
def element_selected?(element)
459
execute :is_element_selected, id: element
460
end
461
462
def element_displayed?(element)
463
WebDriver.logger.debug 'Using script for :isDisplayed', id: :script
464
execute_atom :isDisplayed, element
465
end
466
467
def element_value_of_css_property(element, prop)
468
execute :get_element_css_value, id: element, property_name: prop
469
end
470
471
#
472
# finding elements
473
#
474
475
def active_element
476
Bridge.element_class.new self, element_id_from(execute(:get_active_element))
477
end
478
479
alias switch_to_active_element active_element
480
481
def find_element_by(how, what, parent_ref = [])
482
how, what = @locator_converter.convert(how, what)
483
484
return execute_atom(:findElements, Support::RelativeLocator.new(what).as_json).first if how == 'relative'
485
486
parent_type, parent_id = parent_ref
487
id = case parent_type
488
when :element
489
execute :find_child_element, {id: parent_id}, {using: how, value: what.to_s}
490
when :shadow_root
491
execute :find_shadow_child_element, {id: parent_id}, {using: how, value: what.to_s}
492
else
493
execute :find_element, {}, {using: how, value: what.to_s}
494
end
495
496
Bridge.element_class.new self, element_id_from(id)
497
end
498
499
def find_elements_by(how, what, parent_ref = [])
500
how, what = @locator_converter.convert(how, what)
501
502
return execute_atom :findElements, Support::RelativeLocator.new(what).as_json if how == 'relative'
503
504
parent_type, parent_id = parent_ref
505
ids = case parent_type
506
when :element
507
execute :find_child_elements, {id: parent_id}, {using: how, value: what.to_s}
508
when :shadow_root
509
execute :find_shadow_child_elements, {id: parent_id}, {using: how, value: what.to_s}
510
else
511
execute :find_elements, {}, {using: how, value: what.to_s}
512
end
513
514
ids.map { |id| Bridge.element_class.new self, element_id_from(id) }
515
end
516
517
def shadow_root(element)
518
id = execute :get_element_shadow_root, id: element
519
ShadowRoot.new self, shadow_root_id_from(id)
520
end
521
522
#
523
# virtual-authenticator
524
#
525
526
def add_virtual_authenticator(options)
527
authenticator_id = execute :add_virtual_authenticator, {}, options.as_json
528
VirtualAuthenticator.new(self, authenticator_id, options)
529
end
530
531
def remove_virtual_authenticator(id)
532
execute :remove_virtual_authenticator, {authenticatorId: id}
533
end
534
535
def add_credential(credential, id)
536
execute :add_credential, {authenticatorId: id}, credential
537
end
538
539
def credentials(authenticator_id)
540
execute :get_credentials, {authenticatorId: authenticator_id}
541
end
542
543
def remove_credential(credential_id, authenticator_id)
544
execute :remove_credential, {credentialId: credential_id, authenticatorId: authenticator_id}
545
end
546
547
def remove_all_credentials(authenticator_id)
548
execute :remove_all_credentials, {authenticatorId: authenticator_id}
549
end
550
551
def user_verified(verified, authenticator_id)
552
execute :set_user_verified, {authenticatorId: authenticator_id}, {isUserVerified: verified}
553
end
554
555
#
556
# federated-credential management
557
#
558
559
def cancel_fedcm_dialog
560
execute :cancel_fedcm_dialog
561
end
562
563
def select_fedcm_account(index)
564
execute :select_fedcm_account, {}, {accountIndex: index}
565
end
566
567
def fedcm_dialog_type
568
execute :get_fedcm_dialog_type
569
end
570
571
def fedcm_title
572
execute(:get_fedcm_title).fetch('title')
573
end
574
575
def fedcm_subtitle
576
execute(:get_fedcm_title).fetch('subtitle', nil)
577
end
578
579
def fedcm_account_list
580
execute :get_fedcm_account_list
581
end
582
583
def fedcm_delay(enabled)
584
execute :set_fedcm_delay, {}, {enabled: enabled}
585
end
586
587
def reset_fedcm_cooldown
588
execute :reset_fedcm_cooldown
589
end
590
591
def click_fedcm_dialog_button
592
execute :click_fedcm_dialog_button, {}, {dialogButton: 'ConfirmIdpLoginContinue'}
593
end
594
595
def bidi
596
msg = 'BiDi must be enabled by setting #web_socket_url to true in options class'
597
raise(WebDriver::Error::WebDriverError, msg)
598
end
599
600
def command_list
601
COMMANDS
602
end
603
604
private
605
606
#
607
# executes a command on the remote server.
608
#
609
# @return [WebDriver::Remote::Response]
610
#
611
612
def execute(command, opts = {}, command_hash = nil)
613
verb, path = commands(command) || raise(ArgumentError, "unknown command: #{command.inspect}")
614
path = path.dup
615
616
path[':session_id'] = session_id if path.include?(':session_id')
617
618
begin
619
opts.each { |key, value| path[key.inspect] = escaper.escape(value.to_s) }
620
rescue IndexError
621
raise ArgumentError, "#{opts.inspect} invalid for #{command.inspect}"
622
end
623
624
WebDriver.logger.debug("-> #{verb.to_s.upcase} #{path}", id: :command)
625
http.call(verb, path, command_hash)['value']
626
end
627
628
def escaper
629
@escaper ||= defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::DEFAULT_PARSER
630
end
631
632
def commands(command)
633
command_list[command] || Bridge.extra_commands[command]
634
end
635
636
def unwrap_script_result(arg)
637
case arg
638
when Array
639
arg.map { |e| unwrap_script_result(e) }
640
when Hash
641
element_id = element_id_from(arg)
642
return Bridge.element_class.new(self, element_id) if element_id
643
644
shadow_root_id = shadow_root_id_from(arg)
645
return ShadowRoot.new self, shadow_root_id if shadow_root_id
646
647
arg.each { |k, v| arg[k] = unwrap_script_result(v) }
648
else
649
arg
650
end
651
end
652
653
def element_id_from(id)
654
id['ELEMENT'] || id[Bridge.element_class::ELEMENT_KEY]
655
end
656
657
def shadow_root_id_from(id)
658
id[ShadowRoot::ROOT_KEY]
659
end
660
661
def prepare_capabilities_payload(capabilities)
662
capabilities = {alwaysMatch: capabilities} if !capabilities['alwaysMatch'] && !capabilities['firstMatch']
663
{capabilities: capabilities}
664
end
665
end # Bridge
666
end # Remote
667
end # WebDriver
668
end # Selenium
669
670