selenium-webdriver 4.17.0 → 4.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +92 -0
  3. data/Gemfile +1 -0
  4. data/README.md +2 -2
  5. data/bin/linux/selenium-manager +0 -0
  6. data/bin/macos/selenium-manager +0 -0
  7. data/bin/windows/selenium-manager.exe +0 -0
  8. data/lib/selenium/server.rb +2 -1
  9. data/lib/selenium/webdriver/atoms/findElements.js +26 -26
  10. data/lib/selenium/webdriver/atoms/getAttribute.js +2 -2
  11. data/lib/selenium/webdriver/atoms/isDisplayed.js +24 -97
  12. data/lib/selenium/webdriver/bidi/log/javascript_log_entry.rb +1 -1
  13. data/lib/selenium/webdriver/bidi/log_handler.rb +63 -0
  14. data/lib/selenium/webdriver/bidi/log_inspector.rb +5 -1
  15. data/lib/selenium/webdriver/bidi/session.rb +7 -7
  16. data/lib/selenium/webdriver/bidi/struct.rb +44 -0
  17. data/lib/selenium/webdriver/bidi.rb +10 -0
  18. data/lib/selenium/webdriver/chrome/service.rb +1 -0
  19. data/lib/selenium/webdriver/chromium/driver.rb +1 -0
  20. data/lib/selenium/webdriver/common/child_process.rb +8 -2
  21. data/lib/selenium/webdriver/common/driver.rb +21 -15
  22. data/lib/selenium/webdriver/common/driver_extensions/has_bidi.rb +1 -1
  23. data/lib/selenium/webdriver/common/driver_extensions/has_fedcm_dialog.rb +55 -0
  24. data/lib/selenium/webdriver/common/driver_finder.rb +66 -14
  25. data/lib/selenium/webdriver/common/error.rb +21 -21
  26. data/lib/selenium/webdriver/common/fedcm/account.rb +50 -0
  27. data/lib/selenium/webdriver/common/fedcm/dialog.rb +74 -0
  28. data/lib/selenium/webdriver/common/fedcm.rb +27 -0
  29. data/lib/selenium/webdriver/common/interactions/pointer_cancel.rb +1 -1
  30. data/lib/selenium/webdriver/common/interactions/wheel_input.rb +1 -1
  31. data/lib/selenium/webdriver/common/local_driver.rb +8 -1
  32. data/lib/selenium/webdriver/common/logger.rb +2 -2
  33. data/lib/selenium/webdriver/common/manager.rb +1 -1
  34. data/lib/selenium/webdriver/common/options.rb +1 -1
  35. data/lib/selenium/webdriver/common/platform.rb +3 -1
  36. data/lib/selenium/webdriver/common/script.rb +45 -0
  37. data/lib/selenium/webdriver/common/search_context.rb +10 -2
  38. data/lib/selenium/webdriver/common/selenium_manager.rb +36 -73
  39. data/lib/selenium/webdriver/common/service.rb +11 -4
  40. data/lib/selenium/webdriver/common/socket_poller.rb +1 -1
  41. data/lib/selenium/webdriver/common/target_locator.rb +1 -2
  42. data/lib/selenium/webdriver/common/wait.rb +1 -1
  43. data/lib/selenium/webdriver/common/websocket_connection.rb +12 -0
  44. data/lib/selenium/webdriver/common.rb +4 -0
  45. data/lib/selenium/webdriver/devtools/network_interceptor.rb +1 -1
  46. data/lib/selenium/webdriver/edge/service.rb +1 -1
  47. data/lib/selenium/webdriver/firefox/options.rb +3 -0
  48. data/lib/selenium/webdriver/firefox/profile.rb +11 -5
  49. data/lib/selenium/webdriver/firefox/profiles_ini.rb +1 -1
  50. data/lib/selenium/webdriver/firefox/service.rb +1 -0
  51. data/lib/selenium/webdriver/ie/options.rb +3 -2
  52. data/lib/selenium/webdriver/ie/service.rb +1 -0
  53. data/lib/selenium/webdriver/remote/bidi_bridge.rb +44 -0
  54. data/lib/selenium/webdriver/remote/bridge/commands.rb +13 -1
  55. data/lib/selenium/webdriver/remote/bridge/locator_converter.rb +76 -0
  56. data/lib/selenium/webdriver/remote/bridge.rb +87 -46
  57. data/lib/selenium/webdriver/remote/capabilities.rb +1 -1
  58. data/lib/selenium/webdriver/remote/http/common.rb +21 -3
  59. data/lib/selenium/webdriver/remote/http/curb.rb +11 -5
  60. data/lib/selenium/webdriver/remote/response.rb +12 -19
  61. data/lib/selenium/webdriver/remote/server_error.rb +1 -1
  62. data/lib/selenium/webdriver/remote.rb +2 -1
  63. data/lib/selenium/webdriver/safari/service.rb +1 -1
  64. data/lib/selenium/webdriver/support/guards/guard.rb +8 -9
  65. data/lib/selenium/webdriver/version.rb +1 -1
  66. data/lib/selenium/webdriver.rb +1 -1
  67. data/selenium-webdriver.gemspec +9 -6
  68. metadata +70 -7
@@ -34,88 +34,33 @@ module Selenium
34
34
  @bin_path ||= '../../../../../bin'
35
35
  end
36
36
 
37
- # @param [Options] options browser options.
38
- # @return [String] the path to the correct driver.
39
- def driver_path(options)
40
- command = generate_command(binary, options)
41
-
42
- output = run(*command)
43
-
44
- browser_path = Platform.cygwin? ? Platform.cygwin_path(output['browser_path']) : output['browser_path']
45
- driver_path = Platform.cygwin? ? Platform.cygwin_path(output['driver_path']) : output['driver_path']
46
- Platform.assert_executable driver_path
47
-
48
- if options.respond_to?(:binary) && browser_path && !browser_path.empty?
49
- options.binary = browser_path
50
- options.browser_version = nil
51
- end
52
-
53
- driver_path
37
+ # @param [Array] arguments what gets sent to to Selenium Manager binary.
38
+ # @return [Hash] paths to the requested assets.
39
+ def binary_paths(*arguments)
40
+ arguments += %w[--language-binding ruby]
41
+ arguments += %w[--output json]
42
+ arguments << '--debug' if WebDriver.logger.debug?
43
+
44
+ run(binary, *arguments)
54
45
  end
55
46
 
56
47
  private
57
48
 
58
- def generate_command(binary, options)
59
- command = [binary, '--browser', options.browser_name]
60
- if options.browser_version
61
- command << '--browser-version'
62
- command << options.browser_version
63
- end
64
- if options.respond_to?(:binary) && !options.binary.nil?
65
- command << '--browser-path'
66
- command << options.binary.gsub('\\', '\\\\\\')
67
- end
68
- if options.proxy
69
- command << '--proxy'
70
- command << (options.proxy.ssl || options.proxy.http)
71
- end
72
- command
73
- end
74
-
75
49
  # @return [String] the path to the correct selenium manager
76
50
  def binary
77
51
  @binary ||= begin
78
- location = ENV.fetch('SE_MANAGER_PATH', begin
79
- directory = File.expand_path(bin_path, __FILE__)
80
- if Platform.windows?
81
- "#{directory}/windows/selenium-manager.exe"
82
- elsif Platform.mac?
83
- "#{directory}/macos/selenium-manager"
84
- elsif Platform.linux?
85
- "#{directory}/linux/selenium-manager"
86
- elsif Platform.unix?
87
- WebDriver.logger.warn('Selenium Manager binary may not be compatible with Unix; verify settings',
88
- id: %i[selenium_manager unix_binary])
89
- "#{directory}/linux/selenium-manager"
90
- end
91
- rescue Error::WebDriverError => e
92
- raise Error::WebDriverError, "Unable to obtain Selenium Manager binary for #{e.message}"
93
- end)
52
+ if (location = ENV.fetch('SE_MANAGER_PATH', nil))
53
+ WebDriver.logger.debug("Selenium Manager set by ENV['SE_MANAGER_PATH']: #{location}")
54
+ end
55
+ location ||= platform_location
94
56
 
95
- validate_location(location)
96
- location
97
- end
98
- end
99
-
100
- def validate_location(location)
101
- begin
102
- Platform.assert_file(location)
103
57
  Platform.assert_executable(location)
104
- rescue TypeError
105
- raise Error::WebDriverError,
106
- "Unable to locate or obtain Selenium Manager binary; #{location} is not a valid file object"
107
- rescue Error::WebDriverError => e
108
- raise Error::WebDriverError, "Selenium Manager binary located, but #{e.message}"
58
+ WebDriver.logger.debug("Selenium Manager binary found at #{location}", id: :selenium_manager)
59
+ location
109
60
  end
110
-
111
- WebDriver.logger.debug("Selenium Manager binary found at #{location}", id: :selenium_manager)
112
61
  end
113
62
 
114
63
  def run(*command)
115
- command += %w[--language-binding ruby]
116
- command += %w[--output json]
117
- command << '--debug' if WebDriver.logger.debug?
118
-
119
64
  WebDriver.logger.debug("Executing Process #{command}", id: :selenium_manager)
120
65
 
121
66
  begin
@@ -124,16 +69,34 @@ module Selenium
124
69
  raise Error::WebDriverError, "Unsuccessful command executed: #{command}; #{e.message}"
125
70
  end
126
71
 
127
- json_output = stdout.empty? ? {} : JSON.parse(stdout)
128
- (json_output['logs'] || []).each do |log|
72
+ json_output = stdout.empty? ? {'logs' => [], 'result' => {}} : JSON.parse(stdout)
73
+ json_output['logs'].each do |log|
129
74
  level = log['level'].casecmp('info').zero? ? 'debug' : log['level'].downcase
130
75
  WebDriver.logger.send(level, log['message'], id: :selenium_manager)
131
76
  end
132
77
 
133
78
  result = json_output['result']
134
- return result unless status.exitstatus.positive?
79
+ return result unless status.exitstatus.positive? || result.nil?
135
80
 
136
- raise Error::WebDriverError, "Unsuccessful command executed: #{command}\n#{result}#{stderr}"
81
+ raise Error::WebDriverError,
82
+ "Unsuccessful command executed: #{command} - Code #{status.exitstatus}\n#{result}\n#{stderr}"
83
+ end
84
+
85
+ def platform_location
86
+ directory = File.expand_path(bin_path, __FILE__)
87
+ if Platform.windows?
88
+ "#{directory}/windows/selenium-manager.exe"
89
+ elsif Platform.mac?
90
+ "#{directory}/macos/selenium-manager"
91
+ elsif Platform.linux?
92
+ "#{directory}/linux/selenium-manager"
93
+ elsif Platform.unix?
94
+ WebDriver.logger.warn('Selenium Manager binary may not be compatible with Unix',
95
+ id: %i[selenium_manager unix_binary])
96
+ "#{directory}/linux/selenium-manager"
97
+ else
98
+ raise Error::WebDriverError, "unsupported platform: #{Platform.os}"
99
+ end
137
100
  end
138
101
  end
139
102
  end # SeleniumManager
@@ -69,6 +69,7 @@ module Selenium
69
69
  def initialize(path: nil, port: nil, log: nil, args: nil)
70
70
  port ||= self.class::DEFAULT_PORT
71
71
  args ||= []
72
+ path ||= env_path
72
73
 
73
74
  @executable_path = path
74
75
  @host = Platform.localhost
@@ -87,16 +88,22 @@ module Selenium
87
88
  end
88
89
 
89
90
  def launch
90
- @executable_path ||= begin
91
- default_options = WebDriver.const_get("#{self.class.name.split('::')[2]}::Options").new
92
- DriverFinder.path(default_options, self.class)
93
- end
91
+ @executable_path ||= env_path || find_driver_path
94
92
  ServiceManager.new(self).tap(&:start)
95
93
  end
96
94
 
97
95
  def shutdown_supported
98
96
  self.class::SHUTDOWN_SUPPORTED
99
97
  end
98
+
99
+ def find_driver_path
100
+ default_options = WebDriver.const_get("#{self.class.name&.split('::')&.[](2)}::Options").new
101
+ DriverFinder.new(default_options, self).driver_path
102
+ end
103
+
104
+ def env_path
105
+ ENV.fetch(self.class::DRIVER_PATH_ENV_KEY, nil)
106
+ end
100
107
  end # Service
101
108
  end # WebDriver
102
109
  end # Selenium
@@ -78,7 +78,7 @@ module Selenium
78
78
  def listening?
79
79
  addr = Socket.getaddrinfo(@host, @port, Socket::AF_INET, Socket::SOCK_STREAM)
80
80
  sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
81
- sockaddr = Socket.pack_sockaddr_in(@port, addr[0][3])
81
+ sockaddr = Socket.pack_sockaddr_in(@port, addr[0][3].to_s)
82
82
 
83
83
  begin
84
84
  sock.connect_nonblock sockaddr
@@ -96,12 +96,11 @@ module Selenium
96
96
  @bridge.switch_to_window id
97
97
 
98
98
  begin
99
- returned = yield
99
+ yield
100
100
  ensure
101
101
  current_handles = @bridge.window_handles
102
102
  original = current_handles.first unless current_handles.include? original
103
103
  @bridge.switch_to_window original
104
- returned
105
104
  end
106
105
  else
107
106
  @bridge.switch_to_window id
@@ -65,7 +65,7 @@ module Selenium
65
65
  msg = if @message
66
66
  @message.dup
67
67
  else
68
- +"timed out after #{@timeout} seconds"
68
+ "timed out after #{@timeout} seconds"
69
69
  end
70
70
 
71
71
  msg << " (#{last_error.message})" if last_error
@@ -52,6 +52,18 @@ module Selenium
52
52
  @callbacks ||= Hash.new { |callbacks, event| callbacks[event] = [] }
53
53
  end
54
54
 
55
+ def add_callback(event, &block)
56
+ callbacks[event] << block
57
+ block.object_id
58
+ end
59
+
60
+ def remove_callback(event, id)
61
+ return if callbacks[event].reject! { |callback| callback.object_id == id }
62
+
63
+ ids = callbacks[event]&.map(&:object_id)
64
+ raise Error::WebDriverError, "Callback with ID #{id} does not exist for event #{event}: #{ids}"
65
+ end
66
+
55
67
  def send_cmd(**payload)
56
68
  id = next_id
57
69
  data = payload.merge(id: id)
@@ -89,6 +89,7 @@ require 'selenium/webdriver/common/driver_extensions/has_pinned_scripts'
89
89
  require 'selenium/webdriver/common/driver_extensions/has_cdp'
90
90
  require 'selenium/webdriver/common/driver_extensions/has_casting'
91
91
  require 'selenium/webdriver/common/driver_extensions/has_launching'
92
+ require 'selenium/webdriver/common/driver_extensions/has_fedcm_dialog'
92
93
  require 'selenium/webdriver/common/keys'
93
94
  require 'selenium/webdriver/common/profile_helper'
94
95
  require 'selenium/webdriver/common/options'
@@ -98,3 +99,6 @@ require 'selenium/webdriver/common/element'
98
99
  require 'selenium/webdriver/common/shadow_root'
99
100
  require 'selenium/webdriver/common/websocket_connection'
100
101
  require 'selenium/webdriver/common/child_process'
102
+ require 'selenium/webdriver/common/script'
103
+ require 'selenium/webdriver/common/fedcm/account'
104
+ require 'selenium/webdriver/common/fedcm/dialog'
@@ -98,7 +98,7 @@ module Selenium
98
98
  original = DevTools::Request.from(id, params)
99
99
  mutable = DevTools::Request.from(id, params)
100
100
 
101
- block.call(mutable) do |&continue| # rubocop:disable Performance/RedundantBlockCall
101
+ block.call(mutable) do |&continue|
102
102
  pending_response_requests[id] = continue
103
103
 
104
104
  if original == mutable
@@ -24,7 +24,7 @@ module Selenium
24
24
  DEFAULT_PORT = 9515
25
25
  EXECUTABLE = 'msedgedriver'
26
26
  SHUTDOWN_SUPPORTED = true
27
-
27
+ DRIVER_PATH_ENV_KEY = 'SE_EDGEDRIVER'
28
28
  def log
29
29
  return @log unless @log.is_a? String
30
30
 
@@ -64,6 +64,9 @@ module Selenium
64
64
 
65
65
  @options[:args] ||= []
66
66
  @options[:prefs] ||= {}
67
+ # Firefox 129 onwards the CDP protocol will not be enabled by default. Setting this preference will enable it.
68
+ # https://fxdx.dev/deprecating-cdp-support-in-firefox-embracing-the-future-with-webdriver-bidi/.
69
+ @options[:prefs]['remote.active-protocols'] = 3
67
70
  @options[:env] ||= {}
68
71
  @options[:log] ||= {level: log_level} if log_level
69
72
 
@@ -24,6 +24,10 @@ module Selenium
24
24
  include ProfileHelper
25
25
 
26
26
  VALID_PREFERENCE_TYPES = [TrueClass, FalseClass, Integer, Float, String].freeze
27
+ WEBDRIVER_PREFS = {
28
+ port: 'webdriver_firefox_port',
29
+ log_file: 'webdriver.log.file'
30
+ }.freeze
27
31
 
28
32
  DEFAULT_PREFERENCES = {
29
33
  'browser.newtabpage.enabled' => false,
@@ -35,8 +39,8 @@ module Selenium
35
39
 
36
40
  LOCK_FILES = %w[.parentlock parent.lock lock].freeze
37
41
 
38
- attr_reader :name, :log_file
39
- attr_writer :secure_ssl, :load_no_focus_lib
42
+ attr_reader :name, :log_file
43
+ attr_writer :secure_ssl, :load_no_focus_lib
40
44
 
41
45
  class << self
42
46
  def ini
@@ -206,8 +210,8 @@ module Selenium
206
210
  File.read(path).split("\n").each do |line|
207
211
  next unless line =~ /user_pref\("([^"]+)"\s*,\s*(.+?)\);/
208
212
 
209
- key = Regexp.last_match(1).strip
210
- value = Regexp.last_match(2).strip
213
+ key = Regexp.last_match(1)&.strip
214
+ value = Regexp.last_match(2)&.strip
211
215
 
212
216
  # wrap the value in an array to make it a valid JSON string.
213
217
  prefs[key] = JSON.parse("[#{value}]").first
@@ -223,7 +227,9 @@ module Selenium
223
227
  end
224
228
  end
225
229
  end
226
- end # Profile
230
+ end
231
+
232
+ # Profile
227
233
  end # Firefox
228
234
  end # WebDriver
229
235
  end # Selenium
@@ -52,7 +52,7 @@ module Selenium
52
52
  when /^\[Profile/
53
53
  name, path = nil if path_for(name, is_relative, path)
54
54
  when /^Name=(.+)$/
55
- name = Regexp.last_match(1).strip
55
+ name = Regexp.last_match(1)&.strip
56
56
  when /^IsRelative=(.+)$/
57
57
  is_relative = Regexp.last_match(1).strip == '1'
58
58
  when /^Path=(.+)$/
@@ -24,6 +24,7 @@ module Selenium
24
24
  DEFAULT_PORT = 4444
25
25
  EXECUTABLE = 'geckodriver'
26
26
  SHUTDOWN_SUPPORTED = false
27
+ DRIVER_PATH_ENV_KEY = 'SE_GECKODRIVER'
27
28
  end # Service
28
29
  end # Firefox
29
30
  end # WebDriver
@@ -42,7 +42,8 @@ module Selenium
42
42
  use_legacy_file_upload_dialog_handling: 'ie.useLegacyFileUploadDialogHandling',
43
43
  attach_to_edge_chrome: 'ie.edgechromium',
44
44
  edge_executable_path: 'ie.edgepath',
45
- ignore_process_match: 'ie.ignoreprocessmatch'
45
+ ignore_process_match: 'ie.ignoreprocessmatch',
46
+ silent: 'silent'
46
47
  }.freeze
47
48
  BROWSER = 'internet explorer'
48
49
 
@@ -81,7 +82,7 @@ module Selenium
81
82
 
82
83
  def initialize(**opts)
83
84
  @args = (opts.delete(:args) || []).to_set
84
- super(**opts)
85
+ super
85
86
 
86
87
  @options[:native_events] = true if @options[:native_events].nil?
87
88
  end
@@ -24,6 +24,7 @@ module Selenium
24
24
  DEFAULT_PORT = 5555
25
25
  EXECUTABLE = 'IEDriverServer'
26
26
  SHUTDOWN_SUPPORTED = true
27
+ DRIVER_PATH_ENV_KEY = 'SE_IEDRIVER'
27
28
  end # Server
28
29
  end # IE
29
30
  end # WebDriver
@@ -0,0 +1,44 @@
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 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
36
+ end
37
+
38
+ def close
39
+ execute(:close_window).tap { |handles| bidi.close if handles.empty? }
40
+ end
41
+ end # BiDiBridge
42
+ end # Remote
43
+ end # WebDriver
44
+ end # Selenium
@@ -155,8 +155,20 @@ module Selenium
155
155
  remove_credential: [:delete,
156
156
  'session/:session_id/webauthn/authenticator/:authenticatorId/credentials/:credentialId'],
157
157
  remove_all_credentials: [:delete, 'session/:session_id/webauthn/authenticator/:authenticatorId/credentials'],
158
- set_user_verified: [:post, 'session/:session_id/webauthn/authenticator/:authenticatorId/uv']
158
+ set_user_verified: [:post, 'session/:session_id/webauthn/authenticator/:authenticatorId/uv'],
159
159
 
160
+ #
161
+ # federated-credential management
162
+ #
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']
160
172
  }.freeze
161
173
  end # Bridge
162
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