selenium-webdriver 4.12.0 → 4.23.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +135 -1
  3. data/Gemfile +1 -0
  4. data/LICENSE +1 -1
  5. data/NOTICE +1 -1
  6. data/README.md +2 -2
  7. data/bin/linux/selenium-manager +0 -0
  8. data/bin/macos/selenium-manager +0 -0
  9. data/bin/windows/selenium-manager.exe +0 -0
  10. data/lib/selenium/server.rb +2 -1
  11. data/lib/selenium/webdriver/atoms/findElements.js +28 -27
  12. data/lib/selenium/webdriver/atoms/getAttribute.js +6 -100
  13. data/lib/selenium/webdriver/atoms/isDisplayed.js +24 -96
  14. data/lib/selenium/webdriver/atoms.rb +6 -3
  15. data/lib/selenium/webdriver/bidi/log/javascript_log_entry.rb +1 -1
  16. data/lib/selenium/webdriver/bidi/log_handler.rb +63 -0
  17. data/lib/selenium/webdriver/bidi/log_inspector.rb +5 -1
  18. data/lib/selenium/webdriver/bidi/session.rb +7 -7
  19. data/lib/selenium/webdriver/bidi/struct.rb +44 -0
  20. data/lib/selenium/webdriver/bidi.rb +10 -0
  21. data/lib/selenium/webdriver/chrome/features.rb +5 -1
  22. data/lib/selenium/webdriver/chrome/service.rb +7 -0
  23. data/lib/selenium/webdriver/chromium/driver.rb +1 -1
  24. data/lib/selenium/webdriver/chromium/features.rb +0 -4
  25. data/lib/selenium/webdriver/common/action_builder.rb +0 -4
  26. data/lib/selenium/webdriver/common/child_process.rb +8 -2
  27. data/lib/selenium/webdriver/common/driver.rb +23 -17
  28. data/lib/selenium/webdriver/common/driver_extensions/has_bidi.rb +1 -1
  29. data/lib/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.rb +55 -0
  30. data/lib/selenium/webdriver/common/driver_extensions/has_file_downloads.rb +65 -0
  31. data/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb +1 -1
  32. data/lib/selenium/webdriver/common/driver_finder.rb +66 -14
  33. data/lib/selenium/webdriver/common/error.rb +22 -22
  34. data/lib/selenium/webdriver/common/fedcm/account.rb +50 -0
  35. data/lib/selenium/webdriver/common/fedcm/dialog.rb +74 -0
  36. data/lib/selenium/webdriver/common/fedcm.rb +27 -0
  37. data/lib/selenium/webdriver/common/interactions/pointer_actions.rb +0 -1
  38. data/lib/selenium/webdriver/common/interactions/pointer_cancel.rb +1 -1
  39. data/lib/selenium/webdriver/common/interactions/wheel_actions.rb +2 -1
  40. data/lib/selenium/webdriver/common/interactions/wheel_input.rb +1 -1
  41. data/lib/selenium/webdriver/common/local_driver.rb +8 -1
  42. data/lib/selenium/webdriver/common/logger.rb +7 -7
  43. data/lib/selenium/webdriver/common/manager.rb +1 -1
  44. data/lib/selenium/webdriver/common/options.rb +6 -2
  45. data/lib/selenium/webdriver/common/platform.rb +7 -1
  46. data/lib/selenium/webdriver/common/proxy.rb +2 -2
  47. data/lib/selenium/webdriver/common/script.rb +45 -0
  48. data/lib/selenium/webdriver/common/search_context.rb +10 -2
  49. data/lib/selenium/webdriver/common/selenium_manager.rb +34 -59
  50. data/lib/selenium/webdriver/common/service.rb +4 -0
  51. data/lib/selenium/webdriver/common/service_manager.rb +1 -1
  52. data/lib/selenium/webdriver/common/socket_poller.rb +1 -1
  53. data/lib/selenium/webdriver/common/takes_screenshot.rb +4 -2
  54. data/lib/selenium/webdriver/common/websocket_connection.rb +12 -0
  55. data/lib/selenium/webdriver/common.rb +5 -2
  56. data/lib/selenium/webdriver/devtools/network_interceptor.rb +1 -1
  57. data/lib/selenium/webdriver/edge/features.rb +5 -1
  58. data/lib/selenium/webdriver/edge/service.rb +7 -0
  59. data/lib/selenium/webdriver/firefox/features.rb +5 -1
  60. data/lib/selenium/webdriver/firefox/options.rb +3 -0
  61. data/lib/selenium/webdriver/firefox/profile.rb +14 -6
  62. data/lib/selenium/webdriver/firefox/profiles_ini.rb +1 -1
  63. data/lib/selenium/webdriver/{common/driver_extensions/has_location.rb → ie/features.rb} +8 -10
  64. data/lib/selenium/webdriver/ie/options.rb +3 -2
  65. data/lib/selenium/webdriver/ie.rb +4 -3
  66. data/lib/selenium/webdriver/{common/driver_extensions/has_network_connection.rb → remote/bidi_bridge.rb} +18 -11
  67. data/lib/selenium/webdriver/remote/bridge/commands.rb +13 -7
  68. data/lib/selenium/webdriver/remote/bridge/locator_converter.rb +76 -0
  69. data/lib/selenium/webdriver/remote/bridge.rb +87 -69
  70. data/lib/selenium/webdriver/remote/capabilities.rb +2 -2
  71. data/lib/selenium/webdriver/remote/driver.rb +4 -0
  72. data/lib/selenium/webdriver/remote/features.rb +75 -0
  73. data/lib/selenium/webdriver/remote/http/common.rb +21 -3
  74. data/lib/selenium/webdriver/remote/response.rb +8 -33
  75. data/lib/selenium/webdriver/remote/server_error.rb +1 -1
  76. data/lib/selenium/webdriver/remote.rb +2 -0
  77. data/lib/selenium/webdriver/safari/features.rb +5 -1
  78. data/lib/selenium/webdriver/support/event_firing_bridge.rb +4 -4
  79. data/lib/selenium/webdriver/support/guards/guard.rb +14 -12
  80. data/lib/selenium/webdriver/support/guards.rb +1 -1
  81. data/lib/selenium/webdriver/version.rb +1 -1
  82. data/lib/selenium/webdriver.rb +1 -1
  83. data/selenium-webdriver.gemspec +9 -7
  84. metadata +84 -6
@@ -17,21 +17,28 @@
17
17
  # specific language governing permissions and limitations
18
18
  # under the License.
19
19
 
20
- # TODO: Deprecated; Delete after 4.0 release
21
20
  module Selenium
22
21
  module WebDriver
23
- module DriverExtensions
24
- module HasNetworkConnection
25
- def network_connection_type
26
- raise Error::UnsupportedOperationError,
27
- 'The W3C standard does not currently support getting network connection'
22
+ module Remote
23
+ class BiDiBridge < Bridge
24
+ attr_reader :bidi
25
+
26
+ def create_session(capabilities)
27
+ super
28
+ socket_url = @capabilities[:web_socket_url]
29
+ @bidi = Selenium::WebDriver::BiDi.new(url: socket_url)
30
+ end
31
+
32
+ def quit
33
+ super
34
+ ensure
35
+ bidi.close
28
36
  end
29
37
 
30
- def network_connection_type=(*)
31
- raise Error::UnsupportedOperationError,
32
- 'The W3C standard does not currently support setting network connection'
38
+ def close
39
+ execute(:close_window).tap { |handles| bidi.close if handles.empty? }
33
40
  end
34
- end # HasNetworkConnection
35
- end # DriverExtensions
41
+ end # BiDiBridge
42
+ end # Remote
36
43
  end # WebDriver
37
44
  end # Selenium
@@ -144,12 +144,6 @@ module Selenium
144
144
  take_screenshot: [:get, 'session/:session_id/screenshot'],
145
145
  take_element_screenshot: [:get, 'session/:session_id/element/:id/screenshot'],
146
146
 
147
- #
148
- # server extensions
149
- #
150
-
151
- upload_file: [:post, 'session/:session_id/se/file'],
152
-
153
147
  #
154
148
  # virtual-authenticator
155
149
  #
@@ -161,8 +155,20 @@ module Selenium
161
155
  remove_credential: [:delete,
162
156
  'session/:session_id/webauthn/authenticator/:authenticatorId/credentials/:credentialId'],
163
157
  remove_all_credentials: [:delete, 'session/:session_id/webauthn/authenticator/:authenticatorId/credentials'],
164
- set_user_verified: [:post, 'session/:session_id/webauthn/authenticator/:authenticatorId/uv']
158
+ set_user_verified: [:post, 'session/:session_id/webauthn/authenticator/:authenticatorId/uv'],
159
+
160
+ #
161
+ # federated-credential management
162
+ #
165
163
 
164
+ get_fedcm_title: [:get, 'session/:session_id/fedcm/gettitle'],
165
+ get_fedcm_dialog_type: [:get, 'session/:session_id/fedcm/getdialogtype'],
166
+ get_fedcm_account_list: [:get, 'session/:session_id/fedcm/accountlist'],
167
+ click_fedcm_dialog_button: [:post, 'session/:session_id/fedcm/clickdialogbutton'],
168
+ cancel_fedcm_dialog: [:post, 'session/:session_id/fedcm/canceldialog'],
169
+ select_fedcm_account: [:post, 'session/:session_id/fedcm/selectaccount'],
170
+ set_fedcm_delay: [:post, 'session/:session_id/fedcm/setdelayenabled'],
171
+ reset_fedcm_cooldown: [:post, 'session/:session_id/fedcm/resetcooldown']
166
172
  }.freeze
167
173
  end # Bridge
168
174
  end # Remote
@@ -0,0 +1,76 @@
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
+ class LocatorConverter
25
+ ESCAPE_CSS_REGEXP = /(['"\\#.:;,!?+<>=~*^$|%&@`{}\-\[\]()])/
26
+ UNICODE_CODE_POINT = 30
27
+
28
+ #
29
+ # Converts a locator to a specification compatible one.
30
+ # @param [String, Symbol] how
31
+ # @param [String] what
32
+ #
33
+
34
+ def convert(how, what)
35
+ how = SearchContext.finders[how.to_sym] || how
36
+
37
+ case how
38
+ when 'class name'
39
+ how = 'css selector'
40
+ what = ".#{escape_css(what.to_s)}"
41
+ when 'id'
42
+ how = 'css selector'
43
+ what = "##{escape_css(what.to_s)}"
44
+ when 'name'
45
+ how = 'css selector'
46
+ what = "*[name='#{escape_css(what.to_s)}']"
47
+ end
48
+
49
+ if what.is_a?(Hash)
50
+ what = what.each_with_object({}) do |(h, w), hash|
51
+ h, w = convert(h.to_s, w)
52
+ hash[h] = w
53
+ end
54
+ end
55
+
56
+ [how, what]
57
+ end
58
+
59
+ private
60
+
61
+ #
62
+ # Escapes invalid characters in CSS selector.
63
+ # @see https://mathiasbynens.be/notes/css-escapes
64
+ #
65
+
66
+ def escape_css(string)
67
+ string = string.gsub(ESCAPE_CSS_REGEXP) { |match| "\\#{match}" }
68
+ string = "\\#{UNICODE_CODE_POINT + Integer(string[0])} #{string[1..]}" if string[0]&.match?(/[[:digit:]]/)
69
+
70
+ string
71
+ end
72
+ end # LocatorConverter
73
+ end # Bridge
74
+ end # Remote
75
+ end # WebDriver
76
+ end # Selenium
@@ -22,6 +22,8 @@ module Selenium
22
22
  module Remote
23
23
  class Bridge
24
24
  autoload :COMMANDS, 'selenium/webdriver/remote/bridge/commands'
25
+ autoload :LocatorConverter, 'selenium/webdriver/remote/bridge/locator_converter'
26
+
25
27
  include Atoms
26
28
 
27
29
  PORT = 4444
@@ -29,6 +31,25 @@ module Selenium
29
31
  attr_accessor :http, :file_detector
30
32
  attr_reader :capabilities
31
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
+
32
53
  #
33
54
  # Initializes the bridge with the given server URL
34
55
  # @param [String, URI] url url for the remote server
@@ -43,6 +64,8 @@ module Selenium
43
64
  @http = http_client || Http::Default.new
44
65
  @http.server_url = uri
45
66
  @file_detector = nil
67
+
68
+ @locator_converter = self.class.locator_converter
46
69
  end
47
70
 
48
71
  #
@@ -60,14 +83,16 @@ module Selenium
60
83
  @capabilities = Capabilities.json_create(capabilities)
61
84
 
62
85
  case @capabilities[:browser_name]
63
- when 'chrome'
86
+ when 'chrome', 'chrome-headless-shell'
64
87
  extend(WebDriver::Chrome::Features)
65
88
  when 'firefox'
66
89
  extend(WebDriver::Firefox::Features)
67
- when 'msedge'
90
+ when 'msedge', 'MicrosoftEdge'
68
91
  extend(WebDriver::Edge::Features)
69
92
  when 'Safari', 'Safari Technology Preview'
70
93
  extend(WebDriver::Safari::Features)
94
+ when 'internet explorer'
95
+ extend(WebDriver::IE::Features)
71
96
  end
72
97
  end
73
98
 
@@ -82,7 +107,7 @@ module Selenium
82
107
  def browser
83
108
  @browser ||= begin
84
109
  name = @capabilities.browser_name
85
- name ? name.tr(' ', '_').downcase.to_sym : 'unknown'
110
+ name ? name.tr(' -', '_').downcase.to_sym : 'unknown'
86
111
  end
87
112
  end
88
113
 
@@ -391,30 +416,11 @@ module Selenium
391
416
  end
392
417
 
393
418
  def send_keys_to_element(element, keys)
394
- # TODO: rework file detectors before Selenium 4.0
395
- if @file_detector
396
- local_files = keys.first&.split("\n")&.map { |key| @file_detector.call(Array(key)) }&.compact
397
- if local_files&.any?
398
- keys = local_files.map { |local_file| upload(local_file) }
399
- keys = Array(keys.join("\n"))
400
- end
401
- end
402
-
403
- # Keep .split(//) for backward compatibility for now
419
+ keys = upload_if_necessary(keys) if @file_detector
404
420
  text = keys.join
405
421
  execute :element_send_keys, {id: element}, {value: text.chars, text: text}
406
422
  end
407
423
 
408
- def upload(local_file)
409
- unless File.file?(local_file)
410
- WebDriver.logger.debug("File detector only works with files. #{local_file.inspect} isn`t a file!",
411
- id: :file_detector)
412
- raise Error::WebDriverError, "You are trying to work with something that isn't a file."
413
- end
414
-
415
- execute :upload_file, {}, {file: Zipper.zip_file(local_file)}
416
- end
417
-
418
424
  def clear_element(element)
419
425
  execute :element_clear, id: element
420
426
  end
@@ -430,7 +436,7 @@ module Selenium
430
436
  "e.initEvent('submit', true, true);\n" \
431
437
  "if (form.dispatchEvent(e)) { HTMLFormElement.prototype.submit.call(form) }\n"
432
438
 
433
- execute_script(script, Element::ELEMENT_KEY => element)
439
+ execute_script(script, Bridge.element_class::ELEMENT_KEY => element)
434
440
  rescue Error::JavascriptError
435
441
  raise Error::UnsupportedOperationError, 'To submit an element, it must be nested inside a form element'
436
442
  end
@@ -517,13 +523,13 @@ module Selenium
517
523
  #
518
524
 
519
525
  def active_element
520
- Element.new self, element_id_from(execute(:get_active_element))
526
+ Bridge.element_class.new self, element_id_from(execute(:get_active_element))
521
527
  end
522
528
 
523
529
  alias switch_to_active_element active_element
524
530
 
525
531
  def find_element_by(how, what, parent_ref = [])
526
- how, what = convert_locator(how, what)
532
+ how, what = @locator_converter.convert(how, what)
527
533
 
528
534
  return execute_atom(:findElements, Support::RelativeLocator.new(what).as_json).first if how == 'relative'
529
535
 
@@ -537,11 +543,11 @@ module Selenium
537
543
  execute :find_element, {}, {using: how, value: what.to_s}
538
544
  end
539
545
 
540
- Element.new self, element_id_from(id)
546
+ Bridge.element_class.new self, element_id_from(id)
541
547
  end
542
548
 
543
549
  def find_elements_by(how, what, parent_ref = [])
544
- how, what = convert_locator(how, what)
550
+ how, what = @locator_converter.convert(how, what)
545
551
 
546
552
  return execute_atom :findElements, Support::RelativeLocator.new(what).as_json if how == 'relative'
547
553
 
@@ -555,7 +561,7 @@ module Selenium
555
561
  execute :find_elements, {}, {using: how, value: what.to_s}
556
562
  end
557
563
 
558
- ids.map { |id| Element.new self, element_id_from(id) }
564
+ ids.map { |id| Bridge.element_class.new self, element_id_from(id) }
559
565
  end
560
566
 
561
567
  def shadow_root(element)
@@ -596,6 +602,55 @@ module Selenium
596
602
  execute :set_user_verified, {authenticatorId: authenticator_id}, {isUserVerified: verified}
597
603
  end
598
604
 
605
+ #
606
+ # federated-credential management
607
+ #
608
+
609
+ def cancel_fedcm_dialog
610
+ execute :cancel_fedcm_dialog
611
+ end
612
+
613
+ def select_fedcm_account(index)
614
+ execute :select_fedcm_account, {}, {accountIndex: index}
615
+ end
616
+
617
+ def fedcm_dialog_type
618
+ execute :get_fedcm_dialog_type
619
+ end
620
+
621
+ def fedcm_title
622
+ execute(:get_fedcm_title).fetch('title')
623
+ end
624
+
625
+ def fedcm_subtitle
626
+ execute(:get_fedcm_title).fetch('subtitle', nil)
627
+ end
628
+
629
+ def fedcm_account_list
630
+ execute :get_fedcm_account_list
631
+ end
632
+
633
+ def fedcm_delay(enabled)
634
+ execute :set_fedcm_delay, {}, {enabled: enabled}
635
+ end
636
+
637
+ def reset_fedcm_cooldown
638
+ execute :reset_fedcm_cooldown
639
+ end
640
+
641
+ def click_fedcm_dialog_button
642
+ execute :click_fedcm_dialog_button, {}, {dialogButton: 'ConfirmIdpLoginContinue'}
643
+ end
644
+
645
+ def bidi
646
+ msg = 'BiDi must be enabled by setting #web_socket_url to true in options class'
647
+ raise(WebDriver::Error::WebDriverError, msg)
648
+ end
649
+
650
+ def command_list
651
+ COMMANDS
652
+ end
653
+
599
654
  private
600
655
 
601
656
  #
@@ -625,7 +680,7 @@ module Selenium
625
680
  end
626
681
 
627
682
  def commands(command)
628
- COMMANDS[command]
683
+ command_list[command] || Bridge.extra_commands[command]
629
684
  end
630
685
 
631
686
  def unwrap_script_result(arg)
@@ -634,7 +689,7 @@ module Selenium
634
689
  arg.map { |e| unwrap_script_result(e) }
635
690
  when Hash
636
691
  element_id = element_id_from(arg)
637
- return Element.new(self, element_id) if element_id
692
+ return Bridge.element_class.new(self, element_id) if element_id
638
693
 
639
694
  shadow_root_id = shadow_root_id_from(arg)
640
695
  return ShadowRoot.new self, shadow_root_id if shadow_root_id
@@ -646,7 +701,7 @@ module Selenium
646
701
  end
647
702
 
648
703
  def element_id_from(id)
649
- id['ELEMENT'] || id[Element::ELEMENT_KEY]
704
+ id['ELEMENT'] || id[Bridge.element_class::ELEMENT_KEY]
650
705
  end
651
706
 
652
707
  def shadow_root_id_from(id)
@@ -657,43 +712,6 @@ module Selenium
657
712
  capabilities = {alwaysMatch: capabilities} if !capabilities['alwaysMatch'] && !capabilities['firstMatch']
658
713
  {capabilities: capabilities}
659
714
  end
660
-
661
- def convert_locator(how, what)
662
- how = SearchContext::FINDERS[how.to_sym] || how
663
-
664
- case how
665
- when 'class name'
666
- how = 'css selector'
667
- what = ".#{escape_css(what.to_s)}"
668
- when 'id'
669
- how = 'css selector'
670
- what = "##{escape_css(what.to_s)}"
671
- when 'name'
672
- how = 'css selector'
673
- what = "*[name='#{escape_css(what.to_s)}']"
674
- end
675
-
676
- if what.is_a?(Hash)
677
- what = what.each_with_object({}) do |(h, w), hash|
678
- h, w = convert_locator(h.to_s, w)
679
- hash[h] = w
680
- end
681
- end
682
-
683
- [how, what]
684
- end
685
-
686
- ESCAPE_CSS_REGEXP = /(['"\\#.:;,!?+<>=~*^$|%&@`{}\-\[\]()])/
687
- UNICODE_CODE_POINT = 30
688
-
689
- # Escapes invalid characters in CSS selector.
690
- # @see https://mathiasbynens.be/notes/css-escapes
691
- def escape_css(string)
692
- string = string.gsub(ESCAPE_CSS_REGEXP) { |match| "\\#{match}" }
693
- string = "\\#{UNICODE_CODE_POINT + Integer(string[0])} #{string[1..]}" if string[0]&.match?(/[[:digit:]]/)
694
-
695
- string
696
- end
697
715
  end # Bridge
698
716
  end # Remote
699
717
  end # WebDriver
@@ -48,7 +48,7 @@ module Selenium
48
48
  @capabilities[key]
49
49
  end
50
50
 
51
- define_method "#{key}=" do |value|
51
+ define_method :"#{key}=" do |value|
52
52
  @capabilities[key] = value
53
53
  end
54
54
  end
@@ -99,7 +99,7 @@ module Selenium
99
99
  end
100
100
 
101
101
  def camel_case(str_or_sym)
102
- str_or_sym.to_s.gsub(/_([a-z])/) { Regexp.last_match(1).upcase }
102
+ str_or_sym.to_s.gsub(/_([a-z])/) { Regexp.last_match(1)&.upcase }
103
103
  end
104
104
 
105
105
  private
@@ -28,6 +28,7 @@ module Selenium
28
28
  class Driver < WebDriver::Driver
29
29
  include DriverExtensions::UploadsFiles
30
30
  include DriverExtensions::HasSessionId
31
+ include DriverExtensions::HasFileDownloads
31
32
 
32
33
  def initialize(capabilities: nil, options: nil, service: nil, url: nil, **opts)
33
34
  raise ArgumentError, "Can not set :service object on #{self.class}" if service
@@ -36,6 +37,9 @@ module Selenium
36
37
  caps = process_options(options, capabilities)
37
38
  super(caps: caps, url: url, **opts)
38
39
  @bridge.file_detector = ->((filename, *)) { File.exist?(filename) && filename.to_s }
40
+ command_list = @bridge.command_list
41
+ @bridge.extend(WebDriver::Remote::Features)
42
+ @bridge.add_commands(command_list)
39
43
  end
40
44
 
41
45
  private
@@ -0,0 +1,75 @@
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
+ module Features
24
+ REMOTE_COMMANDS = {
25
+ upload_file: [:post, 'session/:session_id/se/file'],
26
+ get_downloadable_files: [:get, 'session/:session_id/se/files'],
27
+ download_file: [:post, 'session/:session_id/se/files'],
28
+ delete_downloadable_files: [:delete, 'session/:session_id/se/files']
29
+ }.freeze
30
+
31
+ def add_commands(commands)
32
+ @command_list = command_list.merge(commands)
33
+ end
34
+
35
+ def command_list
36
+ @command_list ||= REMOTE_COMMANDS
37
+ end
38
+
39
+ def commands(command)
40
+ command_list[command]
41
+ end
42
+
43
+ def upload(local_file)
44
+ unless File.file?(local_file)
45
+ WebDriver.logger.error("File detector only works with files. #{local_file.inspect} isn`t a file!",
46
+ id: :file_detector)
47
+ raise Error::WebDriverError, "You are trying to upload something that isn't a file."
48
+ end
49
+
50
+ execute :upload_file, {}, {file: Zipper.zip_file(local_file)}
51
+ end
52
+
53
+ def upload_if_necessary(keys)
54
+ local_files = keys.first&.split("\n")&.filter_map { |key| @file_detector.call(Array(key)) }
55
+ return keys unless local_files&.any?
56
+
57
+ keys = local_files.map { |local_file| upload(local_file) }
58
+ Array(keys.join("\n"))
59
+ end
60
+
61
+ def downloadable_files
62
+ execute :get_downloadable_files
63
+ end
64
+
65
+ def download_file(name)
66
+ execute :download_file, {}, {name: name}
67
+ end
68
+
69
+ def delete_downloadable_files
70
+ execute :delete_downloadable_files
71
+ end
72
+ end
73
+ end # Remote
74
+ end # WebDriver
75
+ end # Selenium
@@ -26,10 +26,18 @@ module Selenium
26
26
  CONTENT_TYPE = 'application/json'
27
27
  DEFAULT_HEADERS = {
28
28
  'Accept' => CONTENT_TYPE,
29
- 'Content-Type' => "#{CONTENT_TYPE}; charset=UTF-8",
30
- 'User-Agent' => "selenium/#{WebDriver::VERSION} (ruby #{Platform.os})"
29
+ 'Content-Type' => "#{CONTENT_TYPE}; charset=UTF-8"
31
30
  }.freeze
32
31
 
32
+ class << self
33
+ attr_accessor :extra_headers
34
+ attr_writer :user_agent
35
+
36
+ def user_agent
37
+ @user_agent ||= "selenium/#{WebDriver::VERSION} (ruby #{Platform.os})"
38
+ end
39
+ end
40
+
33
41
  attr_writer :server_url
34
42
 
35
43
  def quit_errors
@@ -42,7 +50,7 @@ module Selenium
42
50
 
43
51
  def call(verb, url, command_hash)
44
52
  url = server_url.merge(url) unless url.is_a?(URI)
45
- headers = DEFAULT_HEADERS.dup
53
+ headers = common_headers.dup
46
54
  headers['Cache-Control'] = 'no-cache' if verb == :get
47
55
 
48
56
  if command_hash
@@ -61,6 +69,16 @@ module Selenium
61
69
 
62
70
  private
63
71
 
72
+ def common_headers
73
+ @common_headers ||= begin
74
+ headers = DEFAULT_HEADERS.dup
75
+ headers['User-Agent'] = Common.user_agent
76
+ headers = headers.merge(Common.extra_headers || {})
77
+
78
+ headers
79
+ end
80
+ end
81
+
64
82
  def server_url
65
83
  return @server_url if @server_url
66
84
 
@@ -28,7 +28,7 @@ module Selenium
28
28
  attr_reader :code, :payload
29
29
 
30
30
  def initialize(code, payload = nil)
31
- @code = code
31
+ @code = code
32
32
  @payload = payload || {}
33
33
 
34
34
  assert_ok
@@ -37,11 +37,8 @@ module Selenium
37
37
  def error
38
38
  error, message, backtrace = process_error
39
39
  klass = Error.for_error(error) || return
40
-
41
40
  ex = klass.new(message)
42
- ex.set_backtrace(caller)
43
- add_backtrace ex, backtrace
44
-
41
+ add_cause(ex, error, backtrace)
45
42
  ex
46
43
  end
47
44
 
@@ -59,34 +56,12 @@ module Selenium
59
56
  raise Error::ServerError, self
60
57
  end
61
58
 
62
- def add_backtrace(ex, server_trace)
63
- return unless server_trace
64
-
65
- backtrace = case server_trace
66
- when Array
67
- backtrace_from_remote(server_trace)
68
- when String
69
- server_trace.split("\n")
70
- end
71
-
72
- ex.set_backtrace(backtrace + ex.backtrace)
73
- end
74
-
75
- def backtrace_from_remote(server_trace)
76
- server_trace.filter_map do |frame|
77
- next unless frame.is_a?(Hash)
78
-
79
- file = frame['fileName']
80
- line = frame['lineNumber']
81
- meth = frame['methodName']
82
-
83
- class_name = frame['className']
84
- file = "#{class_name}(#{file})" if class_name
85
-
86
- meth = 'unknown' if meth.nil? || meth.empty?
87
-
88
- "[remote server] #{file}:#{line}:in `#{meth}'"
89
- end
59
+ def add_cause(ex, error, backtrace)
60
+ cause = Error::WebDriverError.new
61
+ cause.set_backtrace(backtrace)
62
+ raise ex, cause: cause
63
+ rescue Error.for_error(error)
64
+ ex
90
65
  end
91
66
 
92
67
  def process_error
@@ -23,7 +23,7 @@ module Selenium
23
23
  class ServerError < StandardError
24
24
  def initialize(response)
25
25
  if response.is_a? String
26
- super(response)
26
+ super
27
27
  else
28
28
  super("status code #{response.code}; payload #{response.payload}")
29
29
  end
@@ -23,7 +23,9 @@ require 'selenium/webdriver/remote/server_error'
23
23
  module Selenium
24
24
  module WebDriver
25
25
  module Remote
26
+ autoload :Features, 'selenium/webdriver/remote/features'
26
27
  autoload :Bridge, 'selenium/webdriver/remote/bridge'
28
+ autoload :BiDiBridge, 'selenium/webdriver/remote/bidi_bridge'
27
29
  autoload :Driver, 'selenium/webdriver/remote/driver'
28
30
  autoload :Response, 'selenium/webdriver/remote/response'
29
31
  autoload :Capabilities, 'selenium/webdriver/remote/capabilities'
@@ -28,8 +28,12 @@ module Selenium
28
28
  attach_debugger: [:post, 'session/:session_id/apple/attach_debugger']
29
29
  }.freeze
30
30
 
31
+ def command_list
32
+ SAFARI_COMMANDS.merge(self.class::COMMANDS)
33
+ end
34
+
31
35
  def commands(command)
32
- SAFARI_COMMANDS[command] || self.class::COMMANDS[command]
36
+ command_list[command]
33
37
  end
34
38
 
35
39
  def permissions