selenium-webdriver 4.0.2 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +70 -0
  3. data/LICENSE +1 -1
  4. data/NOTICE +1 -1
  5. data/lib/selenium/server.rb +14 -9
  6. data/lib/selenium/webdriver/bidi/session.rb +38 -0
  7. data/lib/selenium/webdriver/bidi.rb +55 -0
  8. data/lib/selenium/webdriver/chrome/features.rb +5 -0
  9. data/lib/selenium/webdriver/chrome/options.rb +20 -20
  10. data/lib/selenium/webdriver/common/action_builder.rb +108 -21
  11. data/lib/selenium/webdriver/common/driver.rb +2 -2
  12. data/lib/selenium/webdriver/common/driver_extensions/has_bidi.rb +38 -0
  13. data/lib/selenium/webdriver/common/driver_extensions/has_casting.rb +10 -0
  14. data/lib/selenium/webdriver/common/driver_extensions/has_context.rb +1 -2
  15. data/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb +1 -1
  16. data/lib/selenium/webdriver/common/driver_extensions/has_pinned_scripts.rb +1 -1
  17. data/lib/selenium/webdriver/common/element.rb +1 -1
  18. data/lib/selenium/webdriver/common/error.rb +1 -1
  19. data/lib/selenium/webdriver/common/interactions/input_device.rb +10 -4
  20. data/lib/selenium/webdriver/common/interactions/interaction.rb +12 -25
  21. data/lib/selenium/webdriver/common/interactions/interactions.rb +24 -4
  22. data/lib/selenium/webdriver/common/interactions/key_actions.rb +5 -1
  23. data/lib/selenium/webdriver/common/interactions/key_input.rb +11 -27
  24. data/lib/selenium/webdriver/common/interactions/none_input.rb +10 -8
  25. data/lib/selenium/webdriver/common/interactions/pause.rb +49 -0
  26. data/lib/selenium/webdriver/common/interactions/pointer_actions.rb +49 -43
  27. data/lib/selenium/webdriver/common/interactions/pointer_cancel.rb +45 -0
  28. data/lib/selenium/webdriver/common/interactions/pointer_event_properties.rb +63 -0
  29. data/lib/selenium/webdriver/common/interactions/pointer_input.rb +15 -84
  30. data/lib/selenium/webdriver/common/interactions/pointer_move.rb +60 -0
  31. data/lib/selenium/webdriver/common/interactions/pointer_press.rb +85 -0
  32. data/lib/selenium/webdriver/common/interactions/scroll.rb +57 -0
  33. data/lib/selenium/webdriver/common/interactions/scroll_origin.rb +48 -0
  34. data/lib/selenium/webdriver/common/interactions/typing_interaction.rb +54 -0
  35. data/lib/selenium/webdriver/common/interactions/wheel_actions.rb +113 -0
  36. data/lib/selenium/webdriver/common/interactions/wheel_input.rb +42 -0
  37. data/lib/selenium/webdriver/common/keys.rb +1 -0
  38. data/lib/selenium/webdriver/common/options.rb +1 -3
  39. data/lib/selenium/webdriver/common/platform.rb +4 -4
  40. data/lib/selenium/webdriver/common/search_context.rb +0 -6
  41. data/lib/selenium/webdriver/common/service_manager.rb +2 -3
  42. data/lib/selenium/webdriver/common/shadow_root.rb +1 -1
  43. data/lib/selenium/webdriver/common/socket_poller.rb +1 -1
  44. data/lib/selenium/webdriver/common/websocket_connection.rb +149 -0
  45. data/lib/selenium/webdriver/common/zipper.rb +3 -1
  46. data/lib/selenium/webdriver/common.rb +14 -2
  47. data/lib/selenium/webdriver/devtools/request.rb +1 -1
  48. data/lib/selenium/webdriver/devtools/response.rb +1 -1
  49. data/lib/selenium/webdriver/devtools.rb +5 -101
  50. data/lib/selenium/webdriver/edge/features.rb +1 -0
  51. data/lib/selenium/webdriver/firefox/driver.rb +1 -0
  52. data/lib/selenium/webdriver/firefox/features.rb +2 -5
  53. data/lib/selenium/webdriver/firefox/options.rb +5 -3
  54. data/lib/selenium/webdriver/firefox/profile.rb +1 -5
  55. data/lib/selenium/webdriver/firefox/util.rb +46 -0
  56. data/lib/selenium/webdriver/firefox.rb +1 -0
  57. data/lib/selenium/webdriver/ie/options.rb +2 -2
  58. data/lib/selenium/webdriver/remote/bridge.rb +25 -20
  59. data/lib/selenium/webdriver/remote/commands.rb +0 -5
  60. data/lib/selenium/webdriver/remote/http/default.rb +6 -12
  61. data/lib/selenium/webdriver/remote/response.rb +2 -2
  62. data/lib/selenium/webdriver/remote.rb +6 -5
  63. data/lib/selenium/webdriver/support/cdp_client_generator.rb +4 -4
  64. data/lib/selenium/webdriver/support/color.rb +7 -7
  65. data/lib/selenium/webdriver/support/guards/guard_condition.rb +1 -1
  66. data/lib/selenium/webdriver/support/guards.rb +1 -1
  67. data/lib/selenium/webdriver/support/select.rb +1 -1
  68. data/lib/selenium/webdriver/version.rb +1 -1
  69. data/lib/selenium/webdriver.rb +2 -1
  70. data/selenium-webdriver.gemspec +7 -4
  71. metadata +55 -5
@@ -45,7 +45,7 @@ module Selenium
45
45
  alias_method :eql?, :==
46
46
 
47
47
  def hash
48
- @id.hash ^ @bridge.hash
48
+ [@id, @bridge].hash
49
49
  end
50
50
 
51
51
  #
@@ -99,7 +99,7 @@ module Selenium
99
99
  end
100
100
 
101
101
  def socket_writable?(sock)
102
- IO.select(nil, [sock], nil, CONNECT_TIMEOUT)
102
+ sock.wait_writable(CONNECT_TIMEOUT)
103
103
  end
104
104
 
105
105
  def conn_completed?(sock)
@@ -0,0 +1,149 @@
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
+ require 'websocket'
21
+
22
+ module Selenium
23
+ module WebDriver
24
+ class WebSocketConnection
25
+ RESPONSE_WAIT_TIMEOUT = 30
26
+ RESPONSE_WAIT_INTERVAL = 0.1
27
+
28
+ def initialize(url:)
29
+ @callback_threads = ThreadGroup.new
30
+
31
+ @messages = []
32
+ @session_id = nil
33
+ @url = url
34
+
35
+ process_handshake
36
+ @socket_thread = attach_socket_listener
37
+ end
38
+
39
+ def close
40
+ @callback_threads.list.each(&:exit)
41
+ @socket_thread.exit
42
+ socket.close
43
+ end
44
+
45
+ def callbacks
46
+ @callbacks ||= Hash.new { |callbacks, event| callbacks[event] = [] }
47
+ end
48
+
49
+ def send_cmd(**payload)
50
+ id = next_id
51
+ data = payload.merge(id: id)
52
+ data = JSON.generate(data)
53
+ WebDriver.logger.debug "WebSocket -> #{data}"
54
+
55
+ out_frame = WebSocket::Frame::Outgoing::Client.new(version: ws.version, data: data, type: 'text')
56
+ socket.write(out_frame.to_s)
57
+
58
+ wait.until { @messages.find { |m| m['id'] == id } }
59
+ end
60
+
61
+ private
62
+
63
+ def process_handshake
64
+ socket.print(ws.to_s)
65
+ ws << socket.readpartial(1024)
66
+ end
67
+
68
+ def attach_socket_listener
69
+ Thread.new do
70
+ Thread.current.abort_on_exception = true
71
+ Thread.current.report_on_exception = false
72
+
73
+ until socket.eof?
74
+ incoming_frame << socket.readpartial(1024)
75
+
76
+ while (frame = incoming_frame.next)
77
+ message = process_frame(frame)
78
+ next unless message['method']
79
+
80
+ params = message['params']
81
+ callbacks[message['method']].each do |callback|
82
+ @callback_threads.add(callback_thread(params, &callback))
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ def incoming_frame
90
+ @incoming_frame ||= WebSocket::Frame::Incoming::Client.new(version: ws.version)
91
+ end
92
+
93
+ def process_frame(frame)
94
+ message = frame.to_s
95
+
96
+ # Firefox will periodically fail on unparsable empty frame
97
+ return {} if message.empty?
98
+
99
+ message = JSON.parse(message)
100
+ @messages << message
101
+ WebDriver.logger.debug "WebSocket <- #{message}"
102
+
103
+ message
104
+ end
105
+
106
+ def callback_thread(params)
107
+ Thread.new do
108
+ Thread.current.abort_on_exception = true
109
+
110
+ # We might end up blocked forever when we have an error in event.
111
+ # For example, if network interception event raises error,
112
+ # the browser will keep waiting for the request to be proceeded
113
+ # before returning back to the original thread. In this case,
114
+ # we should at least print the error.
115
+ Thread.current.report_on_exception = true
116
+
117
+ yield params
118
+ end
119
+ end
120
+
121
+ def wait
122
+ @wait ||= Wait.new(timeout: RESPONSE_WAIT_TIMEOUT, interval: RESPONSE_WAIT_INTERVAL)
123
+ end
124
+
125
+ def socket
126
+ @socket ||= if URI(@url).scheme == 'wss'
127
+ socket = TCPSocket.new(ws.host, ws.port)
128
+ socket = OpenSSL::SSL::SSLSocket.new(socket, OpenSSL::SSL::SSLContext.new)
129
+ socket.sync_close = true
130
+ socket.connect
131
+
132
+ socket
133
+ else
134
+ TCPSocket.new(ws.host, ws.port)
135
+ end
136
+ end
137
+
138
+ def ws
139
+ @ws ||= WebSocket::Handshake::Client.new(url: @url)
140
+ end
141
+
142
+ def next_id
143
+ @id ||= 0
144
+ @id += 1
145
+ end
146
+
147
+ end # BiDi
148
+ end # WebDriver
149
+ end # Selenium
@@ -72,7 +72,9 @@ module Selenium
72
72
  private
73
73
 
74
74
  def with_tmp_zip(&blk)
75
- Tempfile.create do |zip_path|
75
+ # Don't use Tempfile since it lacks rb_file_s_rename permission on Windows.
76
+ Dir.mktmpdir do |tmp_dir|
77
+ zip_path = File.join(tmp_dir, 'webdriver-zip')
76
78
  Zip::File.open(zip_path, Zip::File::CREATE, &blk)
77
79
  end
78
80
  end
@@ -38,14 +38,24 @@ require 'selenium/webdriver/common/logger'
38
38
  require 'selenium/webdriver/common/logs'
39
39
  require 'selenium/webdriver/common/manager'
40
40
  require 'selenium/webdriver/common/search_context'
41
+ require 'selenium/webdriver/common/interactions/interaction'
42
+ require 'selenium/webdriver/common/interactions/interactions'
43
+ require 'selenium/webdriver/common/interactions/pointer_event_properties'
44
+ require 'selenium/webdriver/common/interactions/pointer_cancel'
45
+ require 'selenium/webdriver/common/interactions/pointer_move'
46
+ require 'selenium/webdriver/common/interactions/pointer_press'
47
+ require 'selenium/webdriver/common/interactions/typing_interaction'
48
+ require 'selenium/webdriver/common/interactions/pause'
41
49
  require 'selenium/webdriver/common/interactions/key_actions'
42
50
  require 'selenium/webdriver/common/interactions/pointer_actions'
43
- require 'selenium/webdriver/common/interactions/interactions'
44
51
  require 'selenium/webdriver/common/interactions/input_device'
45
- require 'selenium/webdriver/common/interactions/interaction'
46
52
  require 'selenium/webdriver/common/interactions/none_input'
47
53
  require 'selenium/webdriver/common/interactions/key_input'
48
54
  require 'selenium/webdriver/common/interactions/pointer_input'
55
+ require 'selenium/webdriver/common/interactions/scroll'
56
+ require 'selenium/webdriver/common/interactions/wheel_input'
57
+ require 'selenium/webdriver/common/interactions/scroll_origin'
58
+ require 'selenium/webdriver/common/interactions/wheel_actions'
49
59
  require 'selenium/webdriver/common/action_builder'
50
60
  require 'selenium/webdriver/common/html5/shared_web_storage'
51
61
  require 'selenium/webdriver/common/html5/local_storage'
@@ -66,6 +76,7 @@ require 'selenium/webdriver/common/driver_extensions/prints_page'
66
76
  require 'selenium/webdriver/common/driver_extensions/uploads_files'
67
77
  require 'selenium/webdriver/common/driver_extensions/full_page_screenshot'
68
78
  require 'selenium/webdriver/common/driver_extensions/has_addons'
79
+ require 'selenium/webdriver/common/driver_extensions/has_bidi'
69
80
  require 'selenium/webdriver/common/driver_extensions/has_devtools'
70
81
  require 'selenium/webdriver/common/driver_extensions/has_authentication'
71
82
  require 'selenium/webdriver/common/driver_extensions/has_logs'
@@ -81,3 +92,4 @@ require 'selenium/webdriver/common/takes_screenshot'
81
92
  require 'selenium/webdriver/common/driver'
82
93
  require 'selenium/webdriver/common/element'
83
94
  require 'selenium/webdriver/common/shadow_root'
95
+ require 'selenium/webdriver/common/websocket_connection'
@@ -35,7 +35,7 @@ module Selenium
35
35
  id: id,
36
36
  url: params.dig('request', 'url'),
37
37
  method: params.dig('request', 'method'),
38
- headers: params.dig('request', 'headers'),
38
+ headers: params.dig('request', 'headers').dup,
39
39
  post_data: params.dig('request', 'postData')
40
40
  )
41
41
  end
@@ -35,7 +35,7 @@ module Selenium
35
35
  id: id,
36
36
  code: params['responseStatusCode'],
37
37
  body: (Base64.strict_decode64(encoded_body) if encoded_body),
38
- headers: params['responseHeaders'].each_with_object({}) do |header, hash|
38
+ headers: params.fetch('responseHeaders', []).each_with_object({}) do |header, hash|
39
39
  hash[header['name']] = header['value']
40
40
  end
41
41
  )
@@ -20,9 +20,6 @@
20
20
  module Selenium
21
21
  module WebDriver
22
22
  class DevTools
23
- RESPONSE_WAIT_TIMEOUT = 30
24
- RESPONSE_WAIT_INTERVAL = 0.1
25
-
26
23
  autoload :ConsoleEvent, 'selenium/webdriver/devtools/console_event'
27
24
  autoload :ExceptionEvent, 'selenium/webdriver/devtools/exception_event'
28
25
  autoload :MutationEvent, 'selenium/webdriver/devtools/mutation_event'
@@ -31,41 +28,23 @@ module Selenium
31
28
  autoload :Response, 'selenium/webdriver/devtools/response'
32
29
 
33
30
  def initialize(url:)
34
- @callback_threads = ThreadGroup.new
35
-
36
- @messages = []
31
+ @ws = WebSocketConnection.new(url: url)
37
32
  @session_id = nil
38
- @url = url
39
-
40
- process_handshake
41
- @socket_thread = attach_socket_listener
42
33
  start_session
43
34
  end
44
35
 
45
36
  def close
46
- @callback_threads.list.each(&:exit)
47
- @socket_thread.exit
48
- socket.close
37
+ @ws.close
49
38
  end
50
39
 
51
40
  def callbacks
52
- @callbacks ||= Hash.new { |callbacks, event| callbacks[event] = [] }
41
+ @ws.callbacks
53
42
  end
54
43
 
55
44
  def send_cmd(method, **params)
56
- id = next_id
57
- data = {id: id, method: method, params: params.reject { |_, v| v.nil? }}
45
+ data = {method: method, params: params.compact}
58
46
  data[:sessionId] = @session_id if @session_id
59
- data = JSON.generate(data)
60
- WebDriver.logger.debug "DevTools -> #{data}"
61
-
62
- out_frame = WebSocket::Frame::Outgoing::Client.new(version: ws.version, data: data, type: 'text')
63
- socket.write(out_frame.to_s)
64
-
65
- message = wait.until do
66
- @messages.find { |m| m['id'] == id }
67
- end
68
-
47
+ message = @ws.send_cmd(**data)
69
48
  raise Error::WebDriverError, error_message(message['error']) if message['error']
70
49
 
71
50
  message
@@ -91,32 +70,6 @@ module Selenium
91
70
 
92
71
  private
93
72
 
94
- def process_handshake
95
- socket.print(ws.to_s)
96
- ws << socket.readpartial(1024)
97
- end
98
-
99
- def attach_socket_listener
100
- Thread.new do
101
- Thread.current.abort_on_exception = true
102
- Thread.current.report_on_exception = false
103
-
104
- until socket.eof?
105
- incoming_frame << socket.readpartial(1024)
106
-
107
- while (frame = incoming_frame.next)
108
- message = process_frame(frame)
109
- next unless message['method']
110
-
111
- params = message['params']
112
- callbacks[message['method']].each do |callback|
113
- @callback_threads.add(callback_thread(params, &callback))
114
- end
115
- end
116
- end
117
- end
118
- end
119
-
120
73
  def start_session
121
74
  targets = target.get_targets.dig('result', 'targetInfos')
122
75
  page_target = targets.find { |target| target['type'] == 'page' }
@@ -124,55 +77,6 @@ module Selenium
124
77
  @session_id = session.dig('result', 'sessionId')
125
78
  end
126
79
 
127
- def incoming_frame
128
- @incoming_frame ||= WebSocket::Frame::Incoming::Client.new(version: ws.version)
129
- end
130
-
131
- def process_frame(frame)
132
- message = frame.to_s
133
-
134
- # Firefox will periodically fail on unparsable empty frame
135
- return {} if message.empty?
136
-
137
- message = JSON.parse(message)
138
- @messages << message
139
- WebDriver.logger.debug "DevTools <- #{message}"
140
-
141
- message
142
- end
143
-
144
- def callback_thread(params)
145
- Thread.new do
146
- Thread.current.abort_on_exception = true
147
-
148
- # We might end up blocked forever when we have an error in event.
149
- # For example, if network interception event raises error,
150
- # the browser will keep waiting for the request to be proceeded
151
- # before returning back to the original thread. In this case,
152
- # we should at least print the error.
153
- Thread.current.report_on_exception = true
154
-
155
- yield params
156
- end
157
- end
158
-
159
- def wait
160
- @wait ||= Wait.new(timeout: RESPONSE_WAIT_TIMEOUT, interval: RESPONSE_WAIT_INTERVAL)
161
- end
162
-
163
- def socket
164
- @socket ||= TCPSocket.new(ws.host, ws.port)
165
- end
166
-
167
- def ws
168
- @ws ||= WebSocket::Handshake::Client.new(url: @url)
169
- end
170
-
171
- def next_id
172
- @id ||= 0
173
- @id += 1
174
- end
175
-
176
80
  def error_message(error)
177
81
  [error['code'], error['message'], error['data']].join(': ')
178
82
  end
@@ -30,6 +30,7 @@ module Selenium
30
30
  get_cast_sinks: [:get, 'session/:session_id/ms/cast/get_sinks'],
31
31
  set_cast_sink_to_use: [:post, 'session/:session_id/ms/cast/set_sink_to_use'],
32
32
  start_cast_tab_mirroring: [:post, 'session/:session_id/ms/cast/start_tab_mirroring'],
33
+ start_cast_desktop_mirroring: [:post, 'session/:session_id/ms/cast/start_desktop_mirroring'],
33
34
  get_cast_issue_message: [:get, 'session/:session_id/ms/cast/get_issue_message'],
34
35
  stop_casting: [:post, 'session/:session_id/ms/cast/stop_casting'],
35
36
  send_command: [:post, 'session/:session_id/ms/cdp/execute']
@@ -30,6 +30,7 @@ module Selenium
30
30
  EXTENSIONS = [DriverExtensions::HasAddons,
31
31
  DriverExtensions::FullPageScreenshot,
32
32
  DriverExtensions::HasContext,
33
+ DriverExtensions::HasBiDi,
33
34
  DriverExtensions::HasDevTools,
34
35
  DriverExtensions::HasLogEvents,
35
36
  DriverExtensions::HasNetworkInterception,
@@ -35,12 +35,9 @@ module Selenium
35
35
  end
36
36
 
37
37
  def install_addon(path, temporary)
38
- if @file_detector
39
- local_file = @file_detector.call(path)
40
- path = upload(local_file) if local_file
41
- end
38
+ addon = File.open(path, 'rb') { |crx_file| Base64.strict_encode64 crx_file.read }
42
39
 
43
- payload = {path: path}
40
+ payload = {addon: addon}
44
41
  payload[:temporary] = temporary unless temporary.nil?
45
42
  execute :install_addon, {}, payload
46
43
  end
@@ -25,11 +25,12 @@ module Selenium
25
25
 
26
26
  KEY = 'moz:firefoxOptions'
27
27
 
28
- # see: https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html
28
+ # see: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions
29
29
  CAPABILITIES = {binary: 'binary',
30
30
  args: 'args',
31
31
  log: 'log',
32
32
  prefs: 'prefs',
33
+ env: 'env',
33
34
  android_package: 'androidPackage',
34
35
  android_activity: 'androidActivity',
35
36
  android_device_serial: 'androidDeviceSerial',
@@ -44,7 +45,7 @@ module Selenium
44
45
  #
45
46
  # @example
46
47
  # options = Selenium::WebDriver::Firefox::Options.new(args: ['--host=127.0.0.1'])
47
- # driver = Selenium::WebDriver.for :firefox, options: options
48
+ # driver = Selenium::WebDriver.for :firefox, capabilities: options
48
49
  #
49
50
  # @param [Hash] opts the pre-defined options to create the Firefox::Options with
50
51
  # @option opts [String] :binary Path to the Firefox executable to use
@@ -62,6 +63,7 @@ module Selenium
62
63
 
63
64
  @options[:args] ||= []
64
65
  @options[:prefs] ||= {}
66
+ @options[:env] ||= {}
65
67
  @options[:log] ||= {level: log_level} if log_level
66
68
 
67
69
  process_profile(@options.delete(:profile))
@@ -175,7 +177,7 @@ module Selenium
175
177
  end
176
178
 
177
179
  def camelize?(key)
178
- key != :prefs
180
+ key != "prefs"
179
181
  end
180
182
  end # Options
181
183
  end # Firefox
@@ -96,7 +96,7 @@ module Selenium
96
96
  raise TypeError, "expected one of #{VALID_PREFERENCE_TYPES.inspect}, got #{value.inspect}:#{value.class}"
97
97
  end
98
98
 
99
- if value.is_a?(String) && stringified?(value)
99
+ if value.is_a?(String) && Util.stringified?(value)
100
100
  raise ArgumentError, "preference values must be plain strings: #{key.inspect} => #{value.inspect}"
101
101
  end
102
102
 
@@ -221,10 +221,6 @@ module Selenium
221
221
  end
222
222
  end
223
223
  end
224
-
225
- def stringified?(str)
226
- /^".*"$/.match?(str)
227
- end
228
224
  end # Profile
229
225
  end # Firefox
230
226
  end # WebDriver
@@ -0,0 +1,46 @@
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 Firefox
23
+ # @api private
24
+ module Util
25
+ module_function
26
+
27
+ def app_data_path
28
+ case Platform.os
29
+ when :windows
30
+ "#{ENV.fetch('APPDATA')}\\Mozilla\\Firefox"
31
+ when :macosx
32
+ "#{Platform.home}/Library/Application Support/Firefox"
33
+ when :unix, :linux
34
+ "#{Platform.home}/.mozilla/firefox"
35
+ else
36
+ raise "Unknown os: #{Platform.os}"
37
+ end
38
+ end
39
+
40
+ def stringified?(str)
41
+ str =~ /^".*"$/
42
+ end
43
+ end # Util
44
+ end # Firefox
45
+ end # WebDriver
46
+ end # Selenium
@@ -24,6 +24,7 @@ require 'rexml/document'
24
24
  module Selenium
25
25
  module WebDriver
26
26
  module Firefox
27
+ autoload :Util, 'selenium/webdriver/firefox/util'
27
28
  autoload :Extension, 'selenium/webdriver/firefox/extension'
28
29
  autoload :ProfilesIni, 'selenium/webdriver/firefox/profiles_ini'
29
30
  autoload :Profile, 'selenium/webdriver/firefox/profile'
@@ -52,12 +52,12 @@ module Selenium
52
52
  #
53
53
  # @example
54
54
  # options = Selenium::WebDriver::IE::Options.new(args: ['--host=127.0.0.1'])
55
- # driver = Selenium::WebDriver.for(:ie, options: options)
55
+ # driver = Selenium::WebDriver.for(:ie, capabilities: options)
56
56
  #
57
57
  # @example
58
58
  # options = Selenium::WebDriver::IE::Options.new
59
59
  # options.element_scroll_behavior = Selenium::WebDriver::IE::Options::SCROLL_BOTTOM
60
- # driver = Selenium::WebDriver.for(:ie, options: options)
60
+ # driver = Selenium::WebDriver.for(:ie, capabilities: options)
61
61
  #
62
62
  # @param [Hash] opts the pre-defined options
63
63
  # @option opts [Array<String>] args
@@ -30,8 +30,8 @@ module Selenium
30
30
 
31
31
  #
32
32
  # Initializes the bridge with the given server URL
33
- # @param [String, URI] :url url for the remote server
34
- # @param [Object] :http_client an HTTP client instance that implements the same protocol as Http::Default
33
+ # @param [String, URI] url url for the remote server
34
+ # @param [Object] http_client an HTTP client instance that implements the same protocol as Http::Default
35
35
  # @api private
36
36
  #
37
37
 
@@ -118,7 +118,7 @@ module Selenium
118
118
  end
119
119
 
120
120
  def alert=(keys)
121
- execute :send_alert_text, {}, {value: keys.split(//), text: keys}
121
+ execute :send_alert_text, {}, {value: keys.chars, text: keys}
122
122
  end
123
123
 
124
124
  def alert_text
@@ -146,9 +146,7 @@ module Selenium
146
146
  end
147
147
 
148
148
  def page_source
149
- execute_script('var source = document.documentElement.outerHTML;' \
150
- 'if (!source) { source = new XMLSerializer().serializeToString(document); }' \
151
- 'return source;')
149
+ execute :get_page_source
152
150
  end
153
151
 
154
152
  #
@@ -369,11 +367,8 @@ module Selenium
369
367
  # actions
370
368
  #
371
369
 
372
- def action(async = false)
373
- ActionBuilder.new self,
374
- Interactions.pointer(:mouse, name: 'mouse'),
375
- Interactions.key('keyboard'),
376
- async
370
+ def action(deprecated_async = nil, async: false, devices: [], duration: 250)
371
+ ActionBuilder.new self, nil, nil, deprecated_async, async: async, devices: devices, duration: duration
377
372
  end
378
373
  alias_method :actions, :action
379
374
 
@@ -404,7 +399,7 @@ module Selenium
404
399
  def send_keys_to_element(element, keys)
405
400
  # TODO: rework file detectors before Selenium 4.0
406
401
  if @file_detector
407
- local_files = keys.first.split("\n").map { |key| @file_detector.call(Array(key)) }.compact
402
+ local_files = keys.first&.split("\n")&.map { |key| @file_detector.call(Array(key)) }&.compact
408
403
  if local_files.any?
409
404
  keys = local_files.map { |local_file| upload(local_file) }
410
405
  keys = Array(keys.join("\n"))
@@ -412,8 +407,8 @@ module Selenium
412
407
  end
413
408
 
414
409
  # Keep .split(//) for backward compatibility for now
415
- text = keys.join('')
416
- execute :element_send_keys, {id: element}, {value: text.split(//), text: text}
410
+ text = keys.join
411
+ execute :element_send_keys, {id: element}, {value: text.chars, text: text}
417
412
  end
418
413
 
419
414
  def upload(local_file)
@@ -430,10 +425,19 @@ module Selenium
430
425
  end
431
426
 
432
427
  def submit_element(element)
433
- form = find_element_by('xpath', "./ancestor-or-self::form", [:element, element])
434
- execute_script("var e = arguments[0].ownerDocument.createEvent('Event');" \
435
- "e.initEvent('submit', true, true);" \
436
- 'if (arguments[0].dispatchEvent(e)) { arguments[0].submit() }', form.as_json)
428
+ script = "var form = arguments[0];\n" \
429
+ "while (form.nodeName != \"FORM\" && form.parentNode) {\n" \
430
+ " form = form.parentNode;\n" \
431
+ "}\n" \
432
+ "if (!form) { throw Error('Unable to find containing form element'); }\n" \
433
+ "if (!form.ownerDocument) { throw Error('Unable to find owning document'); }\n" \
434
+ "var e = form.ownerDocument.createEvent('Event');\n" \
435
+ "e.initEvent('submit', true, true);\n" \
436
+ "if (form.dispatchEvent(e)) { HTMLFormElement.prototype.submit.call(form) }\n"
437
+
438
+ execute_script(script, Element::ELEMENT_KEY => element)
439
+ rescue Error::JavascriptError
440
+ raise Error::UnsupportedOperationError, "To submit an element, it must be nested inside a form element"
437
441
  end
438
442
 
439
443
  #
@@ -604,6 +608,9 @@ module Selenium
604
608
  element_id = element_id_from(arg)
605
609
  return Element.new(self, element_id) if element_id
606
610
 
611
+ shadow_root_id = shadow_root_id_from(arg)
612
+ return ShadowRoot.new self, shadow_root_id if shadow_root_id
613
+
607
614
  arg.each { |k, v| arg[k] = unwrap_script_result(v) }
608
615
  else
609
616
  arg
@@ -636,8 +643,6 @@ module Selenium
636
643
  when 'name'
637
644
  how = 'css selector'
638
645
  what = "*[name='#{escape_css(what.to_s)}']"
639
- when 'tag name'
640
- how = 'css selector'
641
646
  end
642
647
 
643
648
  if what.is_a?(Hash)