selenium-webdriver 4.17.0 → 4.26.0

Sign up to get free protection for your applications and to get access to all the features.
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